diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 7580709802..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve CloudBeaver -title: '' -labels: bug, wait for review -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here, e.g. error log. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000000..09005aab23 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,61 @@ +name: 🐛 Bug Report +description: Create a report to help us improve our product +labels: [wait for review] +type: Bug +body: + - type: markdown + attributes: + value: | + Thank you for reporting a bug in CloudBeaver! + To ensure we can address the issue as quickly and efficiently as possible, please provide as much detail as you can in the fields below. + - type: textarea + id: description + attributes: + label: Description + description: | + A clear and concise description of the issue being addressed. + - Describe the actual problematic behavior. + - Ensure private information is redacted. + - Include console output or log files if relevant. + - If applicable, add screenshots or screen recordings to help explain your problem. + placeholder: | + Please enter a description of the issue. Here you can also attach log files, screenshots or a video + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + description: Describe the steps to reproduce the bug + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + - type: textarea + id: expected + attributes: + label: Expected/Desired Behavior + description: | + A clear and concise description of what happens in the software **after** a fix is created and merged. + validations: + required: true + - type: input + id: version + attributes: + label: CloudBeaver Version + description: What version of CloudBeaver are you running? + placeholder: ex. CloudBeaver Community 25.0.0 + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Any additional context about the environment + placeholder: | + Example: + 1. OS: [e.g. iOS, Ubuntu, Windows 11] + 2. Browser: [e.g. Chrome, Safari] + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..b124813ede --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: Read our documentations + url: https://dbeaver.com/docs/cloudbeaver + about: Many articles about product deployment and features. + - name: Try advanced database management features in DBeaver PRO or CloudBeaver EE + url: https://dbeaver.com/download/?utm_source=github&utm_medium=social&utm_campaign=issue + about: Download a 14-day free trial on dbeaver.com. No credit card is needed. + - name: Learn how to use CloudBeaver effectively with our tutorial videos + url: https://www.youtube.com/channel/UC-yOjsQLSaJVEghg6UB3N7A + about: Subscribe to our YouTube channel and watch new tutorials every week. + - name: Follow us on X (ex. Twitter) + url: https://x.com/dbeaver_news + about: Get everyday tips, fresh news, and DBeaver/CloudBeaver feature overviews. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 8c230a5993..766ae59f0a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,8 +1,9 @@ --- -name: Feature request -about: Suggest an idea for CloudBeaver +name: Feature Request +about: Provide a description of your feature request for CloudBeaver. Include the problem you are trying to solve, why it is important, and any specific functionality you believe would improve the platform. title: '' -labels: feature request, wait for review +labels: wait for review +type: Feature assignees: '' --- @@ -12,3 +13,9 @@ A clear and concise description of what the problem is. Ex. I'm always frustrate **Describe the solution you'd like** A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index e340b492ea..40db730440 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,8 +1,9 @@ --- name: Question -about: Is there something unclear? +about: Is there something unclear? Ask your question. title: '' -labels: question, wait for review +labels: wait for review +type: Question assignees: '' --- diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml new file mode 100644 index 0000000000..389934cdd3 --- /dev/null +++ b/.github/workflows/backend-build.yml @@ -0,0 +1,42 @@ +name: Backend Build + +on: + # Allows you to reuse workflows by referencing their YAML files + workflow_call: + +jobs: + build-backend: + name: Build + timeout-minutes: 10 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + path: cloudbeaver + + - name: Clone Deps Repositories + uses: dbeaver/github-actions/clone-repositories@devel + with: + project_deps_path: "./cloudbeaver/project.deps" + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "21" + cache: maven + + - uses: dbeaver/github-actions/install-maven@devel + + - name: Run build script + run: ./build-backend.sh + shell: bash + working-directory: ./cloudbeaver/deploy + + # - name: Archive build artifacts + # uses: actions/upload-artifact@v4 + # with: + # name: backend-build-artifacts + # path: cloudbeaver/deploy/cloudbeaver + # if-no-files-found: error diff --git a/.github/workflows/common-cleanup.yml b/.github/workflows/common-cleanup.yml new file mode 100644 index 0000000000..9acd34dd17 --- /dev/null +++ b/.github/workflows/common-cleanup.yml @@ -0,0 +1,14 @@ +name: Cleanup + +on: + pull_request: + types: [closed] + push: + branches: + - devel + +jobs: + delete-caches: + name: Cleanup + uses: dbeaver/dbeaver-common/.github/workflows/cleanup-caches.yml@devel + secrets: inherit diff --git a/.github/workflows/docker-build-and-push.yml b/.github/workflows/docker-build-and-push.yml new file mode 100644 index 0000000000..19c7b99773 --- /dev/null +++ b/.github/workflows/docker-build-and-push.yml @@ -0,0 +1,93 @@ +name: Build and Push Docker Image + +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + + # Allows you to reuse workflows by referencing their YAML files + workflow_call: + +jobs: + build-and-push-docker: + name: Build and Push Docker Image + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download backend artifacts + uses: actions/download-artifact@v4 + with: + name: backend-build-artifacts + path: deploy/cloudbeaver/ + + - name: Download frontend artifacts + uses: actions/download-artifact@v4 + with: + name: frontend-build-artifacts + path: deploy/cloudbeaver/web + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Run custom Docker build script + run: ./make-docker-container.sh + shell: bash + working-directory: ./deploy/docker + + - name: Tag Docker Image + run: | + REPO_NAME=$(basename ${{ github.repository }}) + IMAGE_NAME=ghcr.io/${{ github.repository_owner }}/$REPO_NAME + BRANCH_NAME=${{ github.head_ref || github.ref_name }} + TAG_NAME=$(echo $BRANCH_NAME | sed 's/[^a-zA-Z0-9._-]/-/g') + docker tag dbeaver/cloudbeaver:dev $IMAGE_NAME:$TAG_NAME + echo "image=$IMAGE_NAME:$TAG_NAME" >> $GITHUB_ENV + + # - name: Install Docker Credential Helper + # run: | + # sudo apt-get update + # sudo apt-get install -y gnupg2 pass + # curl -fsSL https://github.com/docker/docker-credential-helpers/releases/download/v0.6.4/docker-credential-pass-v0.6.4-amd64.tar.gz -o docker-credential-pass.tar.gz + # tar xzvf docker-credential-pass.tar.gz + # sudo mv docker-credential-pass /usr/local/bin/docker-credential-pass + # sudo chmod +x /usr/local/bin/docker-credential-pass + + # - name: Configure Docker to use Credential Helper + # run: | + # mkdir -p ~/.docker + # echo '{"credsStore":"pass"}' > ~/.docker/config.json + + # - name: Initialize Password Store + # run: | + # gpg --batch --gen-key <> $GITHUB_OUTPUT - - - name: restore yarn cache - uses: actions/cache@v3 - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: restore node_modules - uses: actions/cache@v3 - with: - path: "**/node_modules" - key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-node_modules- - - - name: restore typescript cache - uses: actions/cache@v3 - with: - path: "**/packages/*/dist" - key: ${{ runner.os }}-dist-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-dist- - - - name: yarn install - uses: borales/actions-yarn@v4 - with: - dir: webapp - cmd: install - - - name: yarn lerna bootstrap - uses: borales/actions-yarn@v4 - with: - dir: webapp - cmd: lerna bootstrap - - - name: build - uses: borales/actions-yarn@v4 - with: - dir: webapp/packages/product-default - cmd: build - - - name: test - uses: borales/actions-yarn@v4 - with: - dir: webapp - cmd: lerna run test diff --git a/.github/workflows/frontend-cleanup.yaml b/.github/workflows/frontend-cleanup.yaml deleted file mode 100644 index 2cb13f6613..0000000000 --- a/.github/workflows/frontend-cleanup.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: cleanup caches by a branch -on: - pull_request: - types: - - closed - -jobs: - cleanup: - runs-on: ubuntu-latest - steps: - - name: Cleanup - run: | - gh extension install actions/gh-actions-cache - - echo "Fetching list of cache key" - cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) - - ## Setting this to not fail the workflow while deleting cache keys. - set +e - echo "Deleting caches..." - for cacheKey in $cacheKeysForPR - do - gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm - done - echo "Done" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge diff --git a/.github/workflows/frontend-lint.yml b/.github/workflows/frontend-lint.yml new file mode 100644 index 0000000000..e927bcec67 --- /dev/null +++ b/.github/workflows/frontend-lint.yml @@ -0,0 +1,46 @@ +name: Frontend Lint + +on: + # Allows you to reuse workflows by referencing their YAML files + workflow_call: + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + + defaults: + run: + working-directory: ./cloudbeaver/webapp + + steps: + - name: Checkout cloudbeaver + uses: actions/checkout@v4 + with: + path: cloudbeaver + + - run: corepack enable + - uses: actions/setup-node@v4 + with: + node-version: "lts/*" + cache: "yarn" + cache-dependency-path: "./cloudbeaver/webapp/yarn.lock" + + # - run: yarn install --immutable + # working-directory: ./cloudbeaver/webapp/common-typescript + + # - run: yarn install --immutable + # working-directory: ./cloudbeaver/webapp/common-react + + - run: | + yarn install --immutable + git fetch origin "${{ github.base_ref }}" --depth=1 + FILES=$(git diff --name-only 'origin/${{ github.base_ref }}' ${{ github.sha }} -- . | sed 's|^webapp/||') + if [ -n "$FILES" ]; then + yarn lint --pass-on-no-patterns --no-error-on-unmatched-pattern $FILES + else + echo "No files to lint" + fi diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml deleted file mode 100644 index 9886e09429..0000000000 --- a/.github/workflows/frontend.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: frontend - -on: - # Triggers the workflow on push or pull request events but only for the "devel" branch - pull_request: - branches: ["devel"] - paths: - - "webapp/**" # monitor for the frontend changes - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - - # Allows you to reuse workflows by referencing their YAML files - workflow_call: - -jobs: - build_and_test: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./webapp - - steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-node@v3 - with: - node-version: "18" - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - - name: restore yarn cache - uses: actions/cache@v3 - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: restore node_modules - uses: actions/cache@v3 - with: - path: "**/node_modules" - key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-node_modules- - - - name: restore typescript cache - uses: actions/cache@v3 - with: - path: "**/packages/*/dist" - key: ${{ runner.os }}-dist-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-dist- - - - name: yarn install - uses: borales/actions-yarn@v4 - with: - dir: webapp - cmd: install - - - name: yarn lerna bootstrap - uses: borales/actions-yarn@v4 - with: - dir: webapp - cmd: lerna bootstrap - - - name: build - uses: borales/actions-yarn@v4 - with: - dir: webapp/packages/product-default - cmd: build - - - name: test - uses: borales/actions-yarn@v4 - with: - dir: webapp - cmd: lerna run test diff --git a/.github/workflows/push-pr-devel.yml b/.github/workflows/push-pr-devel.yml new file mode 100644 index 0000000000..ae7ee9c118 --- /dev/null +++ b/.github/workflows/push-pr-devel.yml @@ -0,0 +1,64 @@ +name: CI + +on: + pull_request: + types: + - opened + - synchronize + - reopened + push: + branches: [devel] + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref_name }} + cancel-in-progress: true + +jobs: + # build-server: + # uses: dbeaver/dbeaver-common/.github/workflows/mvn-package.yml@devel + # name: Check + # secrets: inherit + # with: + # mvn-args: -Dplain-api-server -Dheadless-platform + # project-directory: ./cloudbeaver/server/product/aggregate + # project-deps: ./cloudbeaver/project.deps + # timeout-minutes: 5 + + # build-server: + # name: Server + # uses: ./.github/workflows/backend-build.yml + # secrets: inherit + + # build-frontend: + # name: Frontend + # uses: ./.github/workflows/frontend-build.yml + # secrets: inherit + + lint-server: + name: Server + uses: dbeaver/dbeaver-common/.github/workflows/java-checkstyle.yml@devel + secrets: inherit + + lint-frontend: + name: Frontend + uses: ./.github/workflows/frontend-lint.yml + secrets: inherit + + # call-frontend-tests: + # name: Frontend Unit Tests + # needs: call-frontend-build + # runs-on: ubuntu-latest + # steps: + # - name: Check if tests passed + # if: ${{ needs.call-frontend-build.outputs.test-status != 'success' }} + # run: | + # echo "Tests failed" + # exit 1 + # - name: Continue if tests passed + # if: ${{ needs.call-frontend-build.outputs.test-status == 'success' }} + # run: echo "Tests passed" + + # call-docker-build-and-push: + # name: Run + # needs: [call-backend-build, call-frontend-build] + # uses: ./.github/workflows/docker-build-and-push.yml diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml deleted file mode 100644 index 9ccd4113b7..0000000000 --- a/.github/workflows/validation.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: validation - -on: - pull_request: - branches: - - devel - types: - - opened - - synchronize - - reopened - - edited - - ready_for_review - - labeled - -jobs: - commit-message: - uses: dbeaver/dbeaver/.github/workflows/reused-commit-msgs-validator.yml@devel diff --git a/.gitignore b/.gitignore index 3524245593..d3f413121b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ dist/ *.iml /idea/ -.idea/ + ## Mac OS .DS_Store @@ -40,6 +40,11 @@ server/test/io.cloudbeaver.test.platform/workspace/.data/ .classpath .settings/ +## Eclipse PDE +*.product.launch + +workspace-dev-ce/ deploy/cloudbeaver server/**/target -apps/**/target \ No newline at end of file +apps/**/target +osgi-cache/ \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1db4798379..32c4a6794d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -15,11 +15,11 @@ "streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker-russian", "syler.sass-indented", - "VisualStudioExptTeam.intellicode-api-usage-examples", "VisualStudioExptTeam.vscodeintellicode", "yzhang.markdown-all-in-one", "GraphQL.vscode-graphql-syntax", "mrmlnc.vscode-json5", - "esbenp.prettier-vscode" + "esbenp.prettier-vscode", + "devnaumov.licensify" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index d2bb0f29a8..291242cde3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,86 +4,73 @@ { "type": "chrome", "request": "launch", - "name": "Chrome", - "url": "http://127.0.0.1:3100", + "name": "CloudBeaver CE", + "url": "http://localhost:8080", "webRoot": "${workspaceFolder}/..", - "sourceMaps": true + "outFiles": [ + "${workspaceFolder}/../cloudbeaver/webapp/packages/**/dist/**/*.{js,jsx}" + ], + "smartStep": true, + "sourceMaps": true, + "disableNetworkCache": true, + "skipFiles": ["/**", "**/node_modules/**"] }, { "type": "java", - "name": "CloudBeaver CE", - "cwd": "${workspaceFolder}/../opt/cbce", + "name": "CloudBeaver CE Server", + "cwd": "${workspaceFolder}/../dbeaver-workspace/app-workspace/cbce", "request": "launch", - "mainClass": "org.eclipse.equinox.launcher.Main", - "windows": { - "type": "java", - "name": "CloudBeaver CE", - "request": "launch", - "mainClass": "org.eclipse.equinox.launcher.Main", - "classPaths": [ - "${workspaceFolder}/../eclipse/eclipse/plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar" - ], - "args": [ - "-product", - "io.cloudbeaver.product.ce.product", - "-configuration", - "file:${workspaceFolder}/../eclipse/workspace/.metadata/.plugins/org.eclipse.pde.core/CloudbeaverServer.product/", - "-dev", - "file:${workspaceFolder}/../eclipse/workspace/.metadata/.plugins/org.eclipse.pde.core/CloudbeaverServer.product/dev.properties", - "-os", - "win32", - "-ws", - "win32", - "-arch", - "x86_64", - "-nl", - "en", - "-showsplash", - "-web-config", - "conf/cloudbeaver.conf" - ], - "vmArgs": [ - "-XX:+IgnoreUnrecognizedVMOptions", - "--add-modules=ALL-SYSTEM", - "-Xms64m", - "-Xmx1024m", - "-Declipse.pde.launch=true" - ] - }, - "osx": { - "type": "java", - "name": "CloudBeaver CE", - "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" - ], - "args": [ - "-product", - "io.cloudbeaver.product.ce.product", - "-configuration", - "file:${workspaceFolder}/../eclipse/workspace/.metadata/.plugins/org.eclipse.pde.core/CloudbeaverServer.product/", - "-dev", - "file:${workspaceFolder}/../eclipse/workspace/.metadata/.plugins/org.eclipse.pde.core/CloudbeaverServer.product/dev.properties", - "-os", - "macosx", - "-ws", - "cocoa", - "-arch", - "aarch64", - "-nl", - "en_US", - "-showsplash" - ], - "vmArgs": [ - "-XX:+IgnoreUnrecognizedVMOptions", - "--add-modules=ALL-SYSTEM", - "-Xms64m", - "-Xmx1024m", - "-Declipse.pde.launch=true", - "-XstartOnFirstThread" - ] - } + "mainClass": "org.jkiss.dbeaver.launcher.DBeaverLauncher", + "args": [ + "-product", + "io.cloudbeaver.product.ce.product", + "-configuration", + "file:${workspaceFolder}/../dbeaver-workspace/products/CloudbeaverServer.product/", + "-dev", + "file:${workspaceFolder}/../dbeaver-workspace/products/CloudbeaverServer.product/dev.properties", + "-nl", + "en", + "-web-config", + "conf/cloudbeaver.conf", + "-registryMultiLanguage" + ], + // "windows": { + // "args": ["-os", "win32", "-ws", "win32", "-arch", "x86_64"] + // }, + // "osx": { + // "args": ["-os", "macosx", "-ws", "cocoa", "-arch", "aarch64"] + // }, + + "vmArgs": [ + "-XX:+IgnoreUnrecognizedVMOptions", + "-Xms64m", + "-Xmx1024m", + "-Declipse.pde.launch=true", + "-XstartOnFirstThread", + "-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", + "--add-opens=java.sql/java.sql=ALL-UNNAMED" + ] } ] } diff --git a/.vscode/license.code-snippets b/.vscode/license.code-snippets deleted file mode 100644 index 3a6dc075e8..0000000000 --- a/.vscode/license.code-snippets +++ /dev/null @@ -1,17 +0,0 @@ -{ - "license": { - "scope": "typescript,typescriptreact,javascript,javascriptreact", - "prefix": "license", - "body": [ - "/*", - " * 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.", - " */", - "", - "" - ] - } -} diff --git a/.vscode/settings.json b/.vscode/settings.json index 21924f722a..4dbb1b68d4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,19 +2,18 @@ "editor.tabSize": 2, "editor.formatOnSave": true, "editor.linkedEditing": true, - "task.autoDetect": "off", - "java.configuration.updateBuildConfiguration": "automatic", "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx16G -Xms100m -Xlog:disable", "java.compile.nullAnalysis.mode": "automatic", - "typescript.preferences.useAliasesForRenames": false, "typescript.preferences.importModuleSpecifier": "relative", "typescript.preferences.includePackageJsonAutoImports": "on", "typescript.preferences.quoteStyle": "single", - "typescript.tsdk": "webapp/node_modules/typescript/lib", - + "typescript.tsdk": "webapp/.yarn/sdks/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "prettier.prettierPath": "webapp/.yarn/sdks/prettier/index.cjs", + "eslint.nodePath": "webapp/.yarn/sdks", "eslint.enable": true, "eslint.validate": [ "javascript", @@ -22,8 +21,9 @@ "typescript", "typescriptreact" ], - "eslint.workingDirectories": ["webapp"], - + "eslint.workingDirectories": [ + "webapp" + ], "editor.defaultFormatter": "esbenp.prettier-vscode", "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" @@ -40,12 +40,10 @@ "[java]": { "editor.defaultFormatter": "redhat.java" }, - "files.eol": "\n", "files.associations": { "*.css": "postcss" }, - - "java.checkstyle.configuration": "${workspaceFolder}/../dbeaver/dbeaver-checkstyle-config.xml", + "java.checkstyle.configuration": "${workspaceFolder}/../dbeaver-common/.github/dbeaver-checkstyle-config.xml", "java.checkstyle.version": "10.12.0" -} +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f35c971de3..93c09dfd67 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,11 +13,31 @@ }, "tasks": [ { - "label": "Run Tests", + "label": "Run Tests CE", "type": "shell", "command": "yarn", "group": "test", - "args": ["lerna", "run", "test", "--no-bail", "--stream", "--"] + "args": ["test"] + }, + { + "label": "Run Tests Common Typescript", + "type": "shell", + "command": "yarn", + "group": "test", + "args": ["test"], + "options": { + "cwd": "${workspaceFolder}/webapp/common-typescript" + } + }, + { + "label": "Run Tests Common React", + "type": "shell", + "command": "yarn", + "group": "test", + "args": ["test"], + "options": { + "cwd": "${workspaceFolder}/webapp/common-react" + } }, { "label": "Run DevServer CE", @@ -33,14 +53,32 @@ "cwd": "${workspaceFolder}/webapp/packages/product-default" } }, + { + "label": "Generate Eclipse PDE CloudBeaver", + "type": "shell", + "osx": { + "command": "./generate_workspace.sh" + }, + "windows": { + "command": "./generate_workspace.cmd" + }, + "options": { + "cwd": "${workspaceFolder}/../cloudbeaver" + }, + "presentation": { + "reveal": "silent", + "close": true, + "clear": false + } + }, { "label": "Build CE", "type": "shell", "windows": { - "command": "./build-sqlite.bat" + "command": "./build.bat" }, "osx": { - "command": "./build-sqlite.sh" + "command": "./build.sh" }, "options": { "cwd": "${workspaceFolder}/deploy" @@ -50,22 +88,54 @@ "isDefault": true } }, + { + "label": "Run Backend CE", + "type": "shell", + "windows": { + "command": "./run-server.bat" + }, + "osx": { + "command": "./run-server.sh" + }, + "options": { + "cwd": "${workspaceFolder}/deploy/cloudbeaver" + } + }, { "label": "Yarn Install", "type": "shell", - "command": "yarn", - "args": [] + "command": "yarn install", + "args": [], + "presentation": { + "reveal": "silent", + "close": true, + "clear": false + } }, { - "label": "Lerna Bootstrap", + "label": "Clean CE", "type": "shell", - "command": "yarn", - "args": ["lerna", "bootstrap"] + "command": "yarn clear", + "options": { + "cwd": "${workspaceFolder}/webapp" + } }, { - "label": "Bootstrap", - "dependsOrder": "sequence", - "dependsOn": ["Yarn Install", "Lerna Bootstrap"] + "label": "Add Custom Plugin", + "type": "shell", + "command": "yarn run add-plugin", + "options": { + "cwd": "${workspaceFolder}/webapp/packages" + } + }, + { + "label": "Run Ui Kit Preview", + "type": "shell", + "command": "yarn", + "options": { + "cwd": "${workspaceFolder}/webapp/common-react/@dbeaver/ui-kit" + }, + "args": ["docs"] } ], "inputs": [ @@ -73,8 +143,8 @@ "type": "pickString", "id": "stage.ce", "description": "CE Stage servers", - "default": "http://127.0.0.1:8978/", - "options": ["http://127.0.0.1:8978/"] + "default": "http://localhost:8978/", + "options": ["http://localhost:8978/"] } ] } diff --git a/.vscode/vscode-java-configs/.editorconfig b/.vscode/vscode-java-configs/.editorconfig new file mode 100644 index 0000000000..1ed453a371 --- /dev/null +++ b/.vscode/vscode-java-configs/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{js,json,yml}] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.vscode/vscode-java-configs/.gitattributes b/.vscode/vscode-java-configs/.gitattributes new file mode 100644 index 0000000000..af3ad12812 --- /dev/null +++ b/.vscode/vscode-java-configs/.gitattributes @@ -0,0 +1,4 @@ +/.yarn/** linguist-vendored +/.yarn/releases/* binary +/.yarn/plugins/**/* binary +/.pnp.* binary linguist-generated diff --git a/.vscode/vscode-java-configs/.gitignore b/.vscode/vscode-java-configs/.gitignore new file mode 100644 index 0000000000..1c43a310b4 --- /dev/null +++ b/.vscode/vscode-java-configs/.gitignore @@ -0,0 +1,14 @@ +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +!.yarn/sdks/** + +# Swap the comments on the following lines if you wish to use zero-installs +# In that case, don't forget to run `yarn config set enableGlobalCache false`! +# Documentation here: https://yarnpkg.com/features/caching#zero-installs + +#!.yarn/cache +.pnp.* diff --git a/.vscode/vscode-java-configs/.vscode/extensions.json b/.vscode/vscode-java-configs/.vscode/extensions.json new file mode 100644 index 0000000000..06dd640c09 --- /dev/null +++ b/.vscode/vscode-java-configs/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "arcanis.vscode-zipfs" + ] +} diff --git a/.vscode/vscode-java-configs/.vscode/settings.json b/.vscode/vscode-java-configs/.vscode/settings.json new file mode 100644 index 0000000000..7629b325de --- /dev/null +++ b/.vscode/vscode-java-configs/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "search.exclude": { + "**/.yarn": true, + "**/.pnp.*": true + }, + "typescript.tsdk": ".yarn/sdks/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} diff --git a/.vscode/vscode-java-configs/.yarn/sdks/integrations.yml b/.vscode/vscode-java-configs/.yarn/sdks/integrations.yml new file mode 100644 index 0000000000..aa9d0d0ad8 --- /dev/null +++ b/.vscode/vscode-java-configs/.yarn/sdks/integrations.yml @@ -0,0 +1,5 @@ +# This file is automatically generated by @yarnpkg/sdks. +# Manual changes might be lost! + +integrations: + - vscode diff --git a/.vscode/vscode-java-configs/.yarn/sdks/typescript/bin/tsc b/.vscode/vscode-java-configs/.yarn/sdks/typescript/bin/tsc new file mode 100755 index 0000000000..867a7bdfe2 --- /dev/null +++ b/.vscode/vscode-java-configs/.yarn/sdks/typescript/bin/tsc @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/bin/tsc + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript/bin/tsc your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`)); diff --git a/.vscode/vscode-java-configs/.yarn/sdks/typescript/bin/tsserver b/.vscode/vscode-java-configs/.yarn/sdks/typescript/bin/tsserver new file mode 100755 index 0000000000..3fc5aa31cc --- /dev/null +++ b/.vscode/vscode-java-configs/.yarn/sdks/typescript/bin/tsserver @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/bin/tsserver + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript/bin/tsserver your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`)); diff --git a/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/tsc.js b/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/tsc.js new file mode 100644 index 0000000000..da411bdba0 --- /dev/null +++ b/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/tsc.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/lib/tsc.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript/lib/tsc.js your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript/lib/tsc.js`)); diff --git a/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/tsserver.js b/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/tsserver.js new file mode 100644 index 0000000000..6249c4675a --- /dev/null +++ b/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/tsserver.js @@ -0,0 +1,248 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/lib/tsserver.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +const moduleWrapper = exports => { + return wrapWithUserWrapper(moduleWrapperFn(exports)); +}; + +const moduleWrapperFn = tsserver => { + if (!process.versions.pnp) { + return tsserver; + } + + const {isAbsolute} = require(`path`); + const pnpApi = require(`pnpapi`); + + const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); + const isPortal = str => str.startsWith("portal:/"); + const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); + + const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { + return `${locator.name}@${locator.reference}`; + })); + + // VSCode sends the zip paths to TS using the "zip://" prefix, that TS + // doesn't understand. This layer makes sure to remove the protocol + // before forwarding it to TS, and to add it back on all returned paths. + + function toEditorPath(str) { + // We add the `zip:` prefix to both `.zip/` paths and virtual paths + if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { + // We also take the opportunity to turn virtual paths into physical ones; + // this makes it much easier to work with workspaces that list peer + // dependencies, since otherwise Ctrl+Click would bring us to the virtual + // file instances instead of the real ones. + // + // We only do this to modules owned by the the dependency tree roots. + // This avoids breaking the resolution when jumping inside a vendor + // with peer dep (otherwise jumping into react-dom would show resolution + // errors on react). + // + const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; + if (resolved) { + const locator = pnpApi.findPackageLocator(resolved); + if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { + str = resolved; + } + } + + str = normalize(str); + + if (str.match(/\.zip\//)) { + switch (hostInfo) { + // Absolute VSCode `Uri.fsPath`s need to start with a slash. + // VSCode only adds it automatically for supported schemes, + // so we have to do it manually for the `zip` scheme. + // The path needs to start with a caret otherwise VSCode doesn't handle the protocol + // + // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 + // + // 2021-10-08: VSCode changed the format in 1.61. + // Before | ^zip:/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + // 2022-04-06: VSCode changed the format in 1.66. + // Before | ^/zip//c:/foo/bar.zip/package.json + // After | ^/zip/c:/foo/bar.zip/package.json + // + // 2022-05-06: VSCode changed the format in 1.68 + // Before | ^/zip/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + case `vscode <1.61`: { + str = `^zip:${str}`; + } break; + + case `vscode <1.66`: { + str = `^/zip/${str}`; + } break; + + case `vscode <1.68`: { + str = `^/zip${str}`; + } break; + + case `vscode`: { + str = `^/zip/${str}`; + } break; + + // To make "go to definition" work, + // We have to resolve the actual file system path from virtual path + // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) + case `coc-nvim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = resolve(`zipfile:${str}`); + } break; + + // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) + // We have to resolve the actual file system path from virtual path, + // everything else is up to neovim + case `neovim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = `zipfile://${str}`; + } break; + + default: { + str = `zip:${str}`; + } break; + } + } else { + str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); + } + } + + return str; + } + + function fromEditorPath(str) { + switch (hostInfo) { + case `coc-nvim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for coc-nvim is in format of //zipfile://.yarn/... + // So in order to convert it back, we use .* to match all the thing + // before `zipfile:` + return process.platform === `win32` + ? str.replace(/^.*zipfile:\//, ``) + : str.replace(/^.*zipfile:/, ``); + } break; + + case `neovim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for neovim is in format of zipfile:////.yarn/... + return str.replace(/^zipfile:\/\//, ``); + } break; + + case `vscode`: + default: { + return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) + } break; + } + } + + // Force enable 'allowLocalPluginLoads' + // TypeScript tries to resolve plugins using a path relative to itself + // which doesn't work when using the global cache + // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 + // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but + // TypeScript already does local loads and if this code is running the user trusts the workspace + // https://github.com/microsoft/vscode/issues/45856 + const ConfiguredProject = tsserver.server.ConfiguredProject; + const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; + ConfiguredProject.prototype.enablePluginsWithOptions = function() { + this.projectService.allowLocalPluginLoads = true; + return originalEnablePluginsWithOptions.apply(this, arguments); + }; + + // And here is the point where we hijack the VSCode <-> TS communications + // by adding ourselves in the middle. We locate everything that looks + // like an absolute path of ours and normalize it. + + const Session = tsserver.server.Session; + const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; + let hostInfo = `unknown`; + + Object.assign(Session.prototype, { + onMessage(/** @type {string | object} */ message) { + const isStringMessage = typeof message === 'string'; + const parsedMessage = isStringMessage ? JSON.parse(message) : message; + + if ( + parsedMessage != null && + typeof parsedMessage === `object` && + parsedMessage.arguments && + typeof parsedMessage.arguments.hostInfo === `string` + ) { + hostInfo = parsedMessage.arguments.hostInfo; + if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { + const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( + // The RegExp from https://semver.org/ but without the caret at the start + /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ + ) ?? []).map(Number) + + if (major === 1) { + if (minor < 61) { + hostInfo += ` <1.61`; + } else if (minor < 66) { + hostInfo += ` <1.66`; + } else if (minor < 68) { + hostInfo += ` <1.68`; + } + } + } + } + + const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { + return typeof value === 'string' ? fromEditorPath(value) : value; + }); + + return originalOnMessage.call( + this, + isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) + ); + }, + + send(/** @type {any} */ msg) { + return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { + return typeof value === `string` ? toEditorPath(value) : value; + }))); + } + }); + + return tsserver; +}; + +const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); +// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. +// Ref https://github.com/microsoft/TypeScript/pull/55326 +if (major > 5 || (major === 5 && minor >= 5)) { + moduleWrapper(absRequire(`typescript`)); +} + +// Defer to the real typescript/lib/tsserver.js your application uses +module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`)); diff --git a/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/tsserverlibrary.js b/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/tsserverlibrary.js new file mode 100644 index 0000000000..0e50e0a2b0 --- /dev/null +++ b/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/tsserverlibrary.js @@ -0,0 +1,248 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/lib/tsserverlibrary.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +const moduleWrapper = exports => { + return wrapWithUserWrapper(moduleWrapperFn(exports)); +}; + +const moduleWrapperFn = tsserver => { + if (!process.versions.pnp) { + return tsserver; + } + + const {isAbsolute} = require(`path`); + const pnpApi = require(`pnpapi`); + + const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); + const isPortal = str => str.startsWith("portal:/"); + const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); + + const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { + return `${locator.name}@${locator.reference}`; + })); + + // VSCode sends the zip paths to TS using the "zip://" prefix, that TS + // doesn't understand. This layer makes sure to remove the protocol + // before forwarding it to TS, and to add it back on all returned paths. + + function toEditorPath(str) { + // We add the `zip:` prefix to both `.zip/` paths and virtual paths + if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { + // We also take the opportunity to turn virtual paths into physical ones; + // this makes it much easier to work with workspaces that list peer + // dependencies, since otherwise Ctrl+Click would bring us to the virtual + // file instances instead of the real ones. + // + // We only do this to modules owned by the the dependency tree roots. + // This avoids breaking the resolution when jumping inside a vendor + // with peer dep (otherwise jumping into react-dom would show resolution + // errors on react). + // + const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; + if (resolved) { + const locator = pnpApi.findPackageLocator(resolved); + if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { + str = resolved; + } + } + + str = normalize(str); + + if (str.match(/\.zip\//)) { + switch (hostInfo) { + // Absolute VSCode `Uri.fsPath`s need to start with a slash. + // VSCode only adds it automatically for supported schemes, + // so we have to do it manually for the `zip` scheme. + // The path needs to start with a caret otherwise VSCode doesn't handle the protocol + // + // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 + // + // 2021-10-08: VSCode changed the format in 1.61. + // Before | ^zip:/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + // 2022-04-06: VSCode changed the format in 1.66. + // Before | ^/zip//c:/foo/bar.zip/package.json + // After | ^/zip/c:/foo/bar.zip/package.json + // + // 2022-05-06: VSCode changed the format in 1.68 + // Before | ^/zip/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + case `vscode <1.61`: { + str = `^zip:${str}`; + } break; + + case `vscode <1.66`: { + str = `^/zip/${str}`; + } break; + + case `vscode <1.68`: { + str = `^/zip${str}`; + } break; + + case `vscode`: { + str = `^/zip/${str}`; + } break; + + // To make "go to definition" work, + // We have to resolve the actual file system path from virtual path + // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) + case `coc-nvim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = resolve(`zipfile:${str}`); + } break; + + // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) + // We have to resolve the actual file system path from virtual path, + // everything else is up to neovim + case `neovim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = `zipfile://${str}`; + } break; + + default: { + str = `zip:${str}`; + } break; + } + } else { + str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); + } + } + + return str; + } + + function fromEditorPath(str) { + switch (hostInfo) { + case `coc-nvim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for coc-nvim is in format of //zipfile://.yarn/... + // So in order to convert it back, we use .* to match all the thing + // before `zipfile:` + return process.platform === `win32` + ? str.replace(/^.*zipfile:\//, ``) + : str.replace(/^.*zipfile:/, ``); + } break; + + case `neovim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for neovim is in format of zipfile:////.yarn/... + return str.replace(/^zipfile:\/\//, ``); + } break; + + case `vscode`: + default: { + return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) + } break; + } + } + + // Force enable 'allowLocalPluginLoads' + // TypeScript tries to resolve plugins using a path relative to itself + // which doesn't work when using the global cache + // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 + // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but + // TypeScript already does local loads and if this code is running the user trusts the workspace + // https://github.com/microsoft/vscode/issues/45856 + const ConfiguredProject = tsserver.server.ConfiguredProject; + const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; + ConfiguredProject.prototype.enablePluginsWithOptions = function() { + this.projectService.allowLocalPluginLoads = true; + return originalEnablePluginsWithOptions.apply(this, arguments); + }; + + // And here is the point where we hijack the VSCode <-> TS communications + // by adding ourselves in the middle. We locate everything that looks + // like an absolute path of ours and normalize it. + + const Session = tsserver.server.Session; + const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; + let hostInfo = `unknown`; + + Object.assign(Session.prototype, { + onMessage(/** @type {string | object} */ message) { + const isStringMessage = typeof message === 'string'; + const parsedMessage = isStringMessage ? JSON.parse(message) : message; + + if ( + parsedMessage != null && + typeof parsedMessage === `object` && + parsedMessage.arguments && + typeof parsedMessage.arguments.hostInfo === `string` + ) { + hostInfo = parsedMessage.arguments.hostInfo; + if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { + const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( + // The RegExp from https://semver.org/ but without the caret at the start + /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ + ) ?? []).map(Number) + + if (major === 1) { + if (minor < 61) { + hostInfo += ` <1.61`; + } else if (minor < 66) { + hostInfo += ` <1.66`; + } else if (minor < 68) { + hostInfo += ` <1.68`; + } + } + } + } + + const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { + return typeof value === 'string' ? fromEditorPath(value) : value; + }); + + return originalOnMessage.call( + this, + isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) + ); + }, + + send(/** @type {any} */ msg) { + return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { + return typeof value === `string` ? toEditorPath(value) : value; + }))); + } + }); + + return tsserver; +}; + +const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); +// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. +// Ref https://github.com/microsoft/TypeScript/pull/55326 +if (major > 5 || (major === 5 && minor >= 5)) { + moduleWrapper(absRequire(`typescript`)); +} + +// Defer to the real typescript/lib/tsserverlibrary.js your application uses +module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`)); diff --git a/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/typescript.js b/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/typescript.js new file mode 100644 index 0000000000..7b6cc22079 --- /dev/null +++ b/.vscode/vscode-java-configs/.yarn/sdks/typescript/lib/typescript.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript`)); diff --git a/.vscode/vscode-java-configs/.yarn/sdks/typescript/package.json b/.vscode/vscode-java-configs/.yarn/sdks/typescript/package.json new file mode 100644 index 0000000000..b4cde731d6 --- /dev/null +++ b/.vscode/vscode-java-configs/.yarn/sdks/typescript/package.json @@ -0,0 +1,10 @@ +{ + "name": "typescript", + "version": "5.8.3-sdk", + "main": "./lib/typescript.js", + "type": "commonjs", + "bin": { + "tsc": "./bin/tsc", + "tsserver": "./bin/tsserver" + } +} diff --git a/.vscode/vscode-java-configs/README.md b/.vscode/vscode-java-configs/README.md new file mode 100644 index 0000000000..5534fdcc01 --- /dev/null +++ b/.vscode/vscode-java-configs/README.md @@ -0,0 +1,7 @@ +# vscode-java-configs + + +## FAQ + +### Typescript Server crashed 5 times + - Open `.pnp.cjs` and replace `bestCandidate.apiPaths.length === 1` with `bestCandidate.apiPaths.length > 0` diff --git a/.vscode/vscode-java-configs/javaConfig.json b/.vscode/vscode-java-configs/javaConfig.json new file mode 100644 index 0000000000..31698aea82 --- /dev/null +++ b/.vscode/vscode-java-configs/javaConfig.json @@ -0,0 +1,3 @@ +{ + "targetPlatform": "./org.jkiss.cloudbeaver.tp.target" +} diff --git a/.vscode/vscode-java-configs/org.jkiss.cloudbeaver.tp.target b/.vscode/vscode-java-configs/org.jkiss.cloudbeaver.tp.target new file mode 100644 index 0000000000..920bcbe5d8 --- /dev/null +++ b/.vscode/vscode-java-configs/org.jkiss.cloudbeaver.tp.target @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.vscode/vscode-java-configs/package.json b/.vscode/vscode-java-configs/package.json new file mode 100644 index 0000000000..90946ecef7 --- /dev/null +++ b/.vscode/vscode-java-configs/package.json @@ -0,0 +1,7 @@ +{ + "name": "vscode-java-configs", + "packageManager": "yarn@4.9.2", + "devDependencies": { + "typescript": "^5.8.3" + } +} diff --git a/.vscode/vscode-java-configs/yarn.lock b/.vscode/vscode-java-configs/yarn.lock new file mode 100644 index 0000000000..e819891aac --- /dev/null +++ b/.vscode/vscode-java-configs/yarn.lock @@ -0,0 +1,34 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"typescript@npm:^5.8.3": + version: 5.8.3 + resolution: "typescript@npm:5.8.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": + version: 5.8.3 + resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb + languageName: node + linkType: hard + +"vscode-java-configs@workspace:.": + version: 0.0.0-use.local + resolution: "vscode-java-configs@workspace:." + dependencies: + typescript: "npm:^5.8.3" + languageName: unknown + linkType: soft diff --git a/README.md b/README.md index 79a6ab72a4..91a23a2cdc 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,61 @@ -# CloudBeaver Community + - +# CloudBeaver Community Cloud Database Manager - Community Edition. -CloudBeaver is a web server which provides rich web interface. Server itself is a Java application, web part is written on TypeScript and React. +CloudBeaver is a web server that provides a rich web interface. The server itself is a Java application, and the web part is written in TypeScript and React. It is free to use and open-source (licensed under [Apache 2](https://github.com/dbeaver/cloudbeaver/blob/devel/LICENSE) license). -See out [WIKI](https://github.com/dbeaver/cloudbeaver/wiki) for more details. +See our [WIKI](https://github.com/dbeaver/cloudbeaver/wiki) for more details. -![](https://github.com/dbeaver/cloudbeaver/wiki/images/demo_screenshot_1.png) + + + + ## Run in Docker - [Official Docker repository](https://hub.docker.com/r/dbeaver/cloudbeaver) -- [Running instructions](https://github.com/dbeaver/cloudbeaver/wiki/Run-Docker-Container) +- [Deployment instructions](https://github.com/dbeaver/cloudbeaver/wiki/CloudBeaver-Deployment) ## Demo server -You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io +You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io [Database access instructions](https://github.com/dbeaver/cloudbeaver/wiki/Demo-Server) ## Changelog -### CloudBeaver 23.2.1 - 2023-09-25 - -- The Output tab has been implemented, which includes warnings, info and notices generated by the database when executing user queries; -- Scrollbars have been made theme-independent, resolving interface issues with light theme; -- Fixed an issue in the SQL editor where it was not possible to switch the active schema when working with Oracle databases; -- Different bug fixes and enhancements have been made. - - -23.2.0 - 2023-09-04 - -- Performance: - - Pagination has been added to the Navigation tree and metadata viewer, allowing working with more than 1000 database items; -- Access Management: - - Access permissions can be specified for pre-configured connections via the configuration file; - - Reverse Proxy authentication now includes the option to provide users' First name and Last name; -- Authorization: - - The SSL option is now available for establishing connections in MySQL, PostgreSQL, Clickhouse; -- Connections: - - Connections are consistently displayed now when they are pre-configured into the workspace in the Global Configuration json file -- Navigation tree: - - The filters for the Navigation tree now allow hiding and showing schemas within the interface. - - The search request considers file names and excludes the .sql file extension for now; -- Data Editor: - - The ability to rearrange columns in the Data Editor has been added; - - The ability to use custom functions in the Grouping panel has been added; +### 25.2.0 2025-09-01 +### Changes since 25.1.0: - Administration: - - New Settings panel displays the product configuration settings such as Minimum fetch size, Maximum fetch size, and Default fetch size from the Data Editor; - - "Connection Management" tab has been renamed to "Connection Templates" - now only connection templates are displayed; + - Added support for multiple server URLs to accommodate different access policies for internal and external users, improving flexibility in network-restricted environments. To set it up, use the Allowed Server URLs field in the Server configuration within the Administration section; + - Added the Force HTTPS mode setting in Server Configuration to enforce redirecting from HTTP to HTTPS. This setting helps avoid a potential man-in-the-middle attack. This setting is turned off by default. Please remember to configure your proxy before enabling; + - Added the CLOUDBEAVER_BIND_SESSION_TO_IP option to improve session security by linking user sessions to their IP address. It is disabled by default. When enabled, this helps protect against certain types of session hijacking attacks, where an attacker could try to take over a user’s session. Note: Users will be logged out automatically on the IP address change (switching networks or using mobile data); + - A password confirmation field has been added for administrators in the Easy Config section to prevent accidental misconfigurations. - SQL Editor: - - Users can simultaneously edit resources for now, allowing them to work together; - - Support for displaying tables with nested arrays of objects has been added; - - The ability to compress export files allowed to increase in download speed, especially for larger files; - - UX in the search bar has been improved - users can delete a query or request by clicking on the cross icon; -- Driver management: - - Custom settings have been added to the interface for the following dialogue connections: SQLite, DB2, SQL Server, MySQL, Oracle and PostgreSQL; - - Trino driver has been updated; -- Security: - - Password autofill functionality was removed; - - The "eye" icon for password fields has been removed - the passwords entered into fields will not be displayed in the interface for now; -- Many small bug fixes, enhancements, and improvements have been made - - -### Old CloudBeaver releases - -You can find information about earlier releases on the CloudBeaver wiki https://github.com/dbeaver/cloudbeaver/wiki/Releases. - + - Changed the default engine used for autocompletion in the SQL Editor. This Semantic engine offers improved suggestions for database objects, keywords, and functions. You can switch back to the Legacy engine in Preferences > SQL Editor; + - A “Clear” button was added to the output panel. +- Data Editor: + - Column descriptions were added in the Data Editor under column names to provide more metadata context. You can disable this in Preferences > Data Viewer; + - Added an ability to review the script before execution when users edit tables without primary keys. +- General: + - Added a new welcome screen for a freshly opened application. This screen contains shortcuts to create a new connection, open SQL Editor, or documentation; + - Added a search panel for SQL Editor and Value panel: press Ctrl/Cmd + F to open a panel that allows searching and replacing by keyword or regular expression; + - Changed the data transfer mechanism to avoid intermediate file creation. The parameter dataExportFileSizeLimit was removed from the server configuration as deprecated; + - The CloudBeaver default theme matches the device theme by default now. You can change this behavior in user preferences under the Theme section; + - Added the option to display tabs across multiple rows, allowing you to see all tabs without scrolling. You can enable this in Preferences > Interface; + - Improved dark theme accessibility by enhancing color contrast across the app for better readability and compliance with accessibility standards; + - The database navigator now automatically hides empty folders in shared projects, reducing visual clutter and speeding up the process of finding active connections. +- Databases and drivers: + - Clickhouse: fixed the presentation of tuples and map data types in the Data Editor; + - Databend database support has been added (thanks to @hantmac); + - DuckDB: driver has been updated to version 1.3; + - MySQL: Improved performance when retrieving foreign keys metadata; + - PostgreSQL: fixed misplaced comment for table DDL generation. + +## Contribution +As a community-driven open-source project, we warmly welcome contributions through GitHub pull requests. + +[We are happy to reward](https://dbeaver.com/help-dbeaver/) our most active contributors every major sprint. +The most significant contribution to our code for the major release 25.2.0 was made by: +1. [hantmac](https://github.com/hantmac) - added support for Databend in CloudBeaver Community Edition. diff --git a/SECURITY.md b/SECURITY.md index 9d72644c1e..094ed1ba00 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,9 +9,10 @@ currently being supported with security updates. | ------- | --------- | | 22.x | yes | | 23.x | yes | +| 24.x | yes | ## Reporting a Vulnerability -Please report (suspected) security vulnerabilities to devops@dbeaver.com. -You will receive a response from us within 48 hours. -If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days. +Please report (suspected) security vulnerabilities to devops@dbeaver.com. +You will receive a response from us within 48 hours. +If the issue is confirmed, we will release a patch as soon as possible, depending on complexity, but historically, within a few days. diff --git a/apps/h2-query-executor/pom.xml b/apps/h2-query-executor/pom.xml index 29169576cb..eae3ac472e 100644 --- a/apps/h2-query-executor/pom.xml +++ b/apps/h2-query-executor/pom.xml @@ -17,7 +17,7 @@ com.h2database h2 - 2.2.220 + 2.1.214 diff --git a/apps/h2-query-executor/src/main/java/io/cloudbeaver/ArgsParser.java b/apps/h2-query-executor/src/main/java/io/cloudbeaver/ArgsParser.java index 745ccd7b63..8acbb9c254 100644 --- a/apps/h2-query-executor/src/main/java/io/cloudbeaver/ArgsParser.java +++ b/apps/h2-query-executor/src/main/java/io/cloudbeaver/ArgsParser.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/apps/h2-query-executor/src/main/java/io/cloudbeaver/H2ExecutorConsoleApplication.java b/apps/h2-query-executor/src/main/java/io/cloudbeaver/H2ExecutorConsoleApplication.java index ce57c4c770..218b8cf1f7 100644 --- a/apps/h2-query-executor/src/main/java/io/cloudbeaver/H2ExecutorConsoleApplication.java +++ b/apps/h2-query-executor/src/main/java/io/cloudbeaver/H2ExecutorConsoleApplication.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/apps/h2-query-executor/src/main/java/io/cloudbeaver/H2QueryExecutor.java b/apps/h2-query-executor/src/main/java/io/cloudbeaver/H2QueryExecutor.java index 94e1f4add5..eea1716a4e 100644 --- a/apps/h2-query-executor/src/main/java/io/cloudbeaver/H2QueryExecutor.java +++ b/apps/h2-query-executor/src/main/java/io/cloudbeaver/H2QueryExecutor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/apps/h2-query-executor/src/main/java/io/cloudbeaver/SqlConsts.java b/apps/h2-query-executor/src/main/java/io/cloudbeaver/SqlConsts.java index 4d7ee13883..2680e5bad0 100644 --- a/apps/h2-query-executor/src/main/java/io/cloudbeaver/SqlConsts.java +++ b/apps/h2-query-executor/src/main/java/io/cloudbeaver/SqlConsts.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/config/GlobalConfiguration/.dbeaver/data-sources.json b/config/GlobalConfiguration/.dbeaver/data-sources.json new file mode 100644 index 0000000000..a5f18e204f --- /dev/null +++ b/config/GlobalConfiguration/.dbeaver/data-sources.json @@ -0,0 +1,4 @@ +{ + "folders": {}, + "connections": {} +} diff --git a/config/sample-databases/DefaultConfiguration/GlobalConfiguration/.dbeaver/provided-connections.json b/config/GlobalConfiguration/.dbeaver/provided-connections.json similarity index 100% rename from config/sample-databases/DefaultConfiguration/GlobalConfiguration/.dbeaver/provided-connections.json rename to config/GlobalConfiguration/.dbeaver/provided-connections.json diff --git a/config/core/cloudbeaver.conf b/config/core/cloudbeaver.conf new file mode 100644 index 0000000000..58b34b5231 --- /dev/null +++ b/config/core/cloudbeaver.conf @@ -0,0 +1,99 @@ +{ + server: { + serverPort: "${CLOUDBEAVER_WEB_SERVER_PORT:8978}", + forceHttps: "${CLOUDBEAVER_FORCE_HTTPS:false}", + + contentRoot: "web", + driversLocation: "drivers", + + sslConfigurationPath:"${CLOUDBEAVER_SSL_CONF_PATH:workspace/.data/ssl-config.xml}", + + rootURI: "${CLOUDBEAVER_ROOT_URI:/}", + serviceURI: "/api/", + supportedHosts: [], + + productSettings: { + # Global properties + core.theming.theme: "${CLOUDBEAVER_CORE_THEMING_THEME:system}", + core.localization.language: "${CLOUDBEAVER_CORE_LOCALIZATION:en}", + plugin.sql-editor.autoSave: "${CLOUDBEAVER_SQL_EDITOR_AUTOSAVE:true}", + plugin.sql-editor.disabled: "${CLOUDBEAVER_SQL_EDITOR_DISABLED:false}", + # max size of the file that can be uploaded to the editor (in kilobytes) + plugin.sql-editor.maxFileSize: "${CLOUDBEAVER_SQL_EDITOR_MAX_FILE_SIZE:10240}", + plugin.log-viewer.disabled: "${CLOUDBEAVER_LOG_VIEWER_DISABLED:false}", + plugin.log-viewer.logBatchSize: "${CLOUDBEAVER_LOG_VIEWER_LOG_BATCH_SIZE:1000}", + plugin.log-viewer.maxLogRecords: "${CLOUDBEAVER_LOG_VIEWER_MAX_LOG_RECORDS:2000}", + sql.proposals.insert.table.alias: "${CLOUDBEAVER_SQL_PROPOSALS_INSERT_TABLE_ALIAS:PLAIN}", + SQLEditor.ContentAssistant.experimental.mode: "${CLOUDBEAVER_SQL_EDITOR_CONTENT_ASSISTANT_EXPERIMENTAL_MODE:NEW}" + }, + + expireSessionAfterPeriod: "${CLOUDBEAVER_EXPIRE_SESSION_AFTER_PERIOD:1800000}", + bindSessionToIp: "${CLOUDBEAVER_BIND_SESSION_TO_IP:disable}", + develMode: "${CLOUDBEAVER_DEVEL_MODE:false}", + + enableSecurityManager: false, + + sm: { + enableBruteForceProtection: "${CLOUDBEAVER_BRUTE_FORCE_PROTECTION_ENABLED:true}", + maxFailedLogin: "${CLOUDBEAVER_MAX_FAILED_LOGINS:10}", + minimumLoginTimeout: "${CLOUDBEAVER_MINIMUM_LOGIN_TIMEOUT:1}", + blockLoginPeriod: "${CLOUDBEAVER_BLOCK_PERIOD:300}", + passwordPolicy: { + minLength: "${CLOUDBEAVER_POLICY_MIN_LENGTH:8}", + requireMixedCase: "${CLOUDBEAVER_POLICY_REQUIRE_MIXED_CASE:true}", + minNumberCount: "${CLOUDBEAVER_POLICY_MIN_NUMBER_COUNT:1}", + minSymbolCount: "${CLOUDBEAVER_POLICY_MIN_SYMBOL_COUNT:0}" + } + }, + + database: { + driver: "${CLOUDBEAVER_DB_DRIVER:h2_embedded_v2}", + url: "${CLOUDBEAVER_DB_URL:jdbc:h2:${workspace}/.data/cb.h2v2.dat}", + schema: "${CLOUDBEAVER_DB_SCHEMA:''}", + user: "${CLOUDBEAVER_DB_USER:''}", + password: "${CLOUDBEAVER_DB_PASSWORD:''}", + initialDataConfiguration: "${CLOUDBEAVER_DB_INITIAL_DATA:conf/initial-data.conf}", + pool: { + minIdleConnections: "${CLOUDBEAVER_DB_MIN_IDLE_CONNECTIONS:4}", + maxIdleConnections: "${CLOUDBEAVER_DB_MAX_IDLE_CONNECTIONS:10}", + maxConnections: "${CLOUDBEAVER_DB_MAX_CONNECTIONS:100}", + validationQuery: "${CLOUDBEAVER_DB_VALIDATION_QUERY:SELECT 1}" + }, + backupEnabled: "${CLOUDBEAVER_DB_BACKUP_ENABLED:true}" + } + + }, + app: { + anonymousAccessEnabled: "${CLOUDBEAVER_APP_ANONYMOUS_ACCESS_ENABLED:true}", + anonymousUserRole: user, + defaultUserTeam: "${CLOUDBEAVER_APP_DEFAULT_USER_TEAM:user}", + grantConnectionsAccessToAnonymousTeam: "${CLOUDBEAVER_APP_GRANT_CONNECTIONS_ACCESS_TO_ANONYMOUS_TEAM:false}", + supportsCustomConnections: "${CLOUDBEAVER_APP_SUPPORTS_CUSTOM_CONNECTIONS:false}", + showReadOnlyConnectionInfo: "${CLOUDBEAVER_APP_READ_ONLY_CONNECTION_INFO:false}", + systemVariablesResolvingEnabled: "${CLOUDBEAVER_SYSTEM_VARIABLES_RESOLVING_ENABLED:false}", + + forwardProxy: "${CLOUDBEAVER_APP_FORWARD_PROXY:false}", + + publicCredentialsSaveEnabled: "${CLOUDBEAVER_APP_PUBLIC_CREDENTIALS_SAVE_ENABLED:true}", + adminCredentialsSaveEnabled: "${CLOUDBEAVER_APP_ADMIN_CREDENTIALS_SAVE_ENABLED:true}", + + resourceManagerEnabled: "${CLOUDBEAVER_APP_RESOURCE_MANAGER_ENABLED:true}", + + resourceQuotas: { + resourceManagerFileSizeLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_RESOURCE_MANAGER_FILE_SIZE_LIMIT:500000}", + sqlMaxRunningQueries: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_MAX_RUNNING_QUERIES:100}", + sqlResultSetRowsLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_RESULT_SET_ROWS_LIMIT:100000}", + sqlTextPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_TEXT_PREVIEW_MAX_LENGTH:4096}", + sqlBinaryPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_BINARY_PREVIEW_MAX_LENGTH:261120}" + }, + enabledAuthProviders: [ + "local" + ], + + disabledBetaFeatures: [ + + ] + + } + +} diff --git a/config/core/initial-data.conf b/config/core/initial-data.conf index b3898fb45f..f0a82b0c8d 100644 --- a/config/core/initial-data.conf +++ b/config/core/initial-data.conf @@ -2,14 +2,14 @@ teams: [ { subjectId: "admin", - name: "Admin", + teamName: "Admin", description: "Administrative access. Has all permissions.", permissions: [ "admin" ] }, { subjectId: "user", - name: "User", - description: "Standard user", + teamName: "User", + description: "All users, including anonymous.", permissions: [ ] } ] diff --git a/config/core/product.conf b/config/core/product.conf deleted file mode 100644 index ebdd856ea3..0000000000 --- a/config/core/product.conf +++ /dev/null @@ -1,34 +0,0 @@ -// Product configuration. Customized web application behavior -// It is in JSONC format -{ - // Global properties - core: { - // User defaults - user: { - defaultTheme: "light", - defaultLanguage: "en" - }, - app: { - // Log viewer config - logViewer: { - refreshTimeout: 3000, - logBatchSize: 1000, - maxLogRecords: 2000, - maxFailedRequests: 3 - } - }, - authentication: { - primaryAuthProvider: 'local' - } - }, - // Notifications config - core_events: { - notificationsPool: 5 - }, - plugin_data_spreadsheet_new: { - hidden: false - }, - plugin_data_export: { - disabled: false - } -} diff --git a/config/sample-databases/DefaultConfiguration/GlobalConfiguration/.dbeaver/data-sources.json b/config/sample-databases/DefaultConfiguration/GlobalConfiguration/.dbeaver/data-sources.json deleted file mode 100644 index c954ec82d9..0000000000 --- a/config/sample-databases/DefaultConfiguration/GlobalConfiguration/.dbeaver/data-sources.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "folders": {}, - "connections": { - "postgresql-template-1": { - "provider": "postgresql", - "driver": "postgres-jdbc", - "name": "PostgreSQL (Template)", - "save-password": false, - "show-system-objects": false, - "read-only": true, - "template": true, - "configuration": { - "host": "localhost", - "port": "5432", - "database": "postgres", - "url": "jdbc:postgresql://localhost:5432/postgres", - "type": "dev", - "provider-properties": { - "@dbeaver-show-non-default-db@": "false" - } - } - } - } -} diff --git a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf b/config/sample-databases/DefaultConfiguration/cloudbeaver.conf deleted file mode 100644 index 20f0e29bd1..0000000000 --- a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf +++ /dev/null @@ -1,69 +0,0 @@ -{ - server: { - serverPort: 8978, - - workspaceLocation: "workspace", - contentRoot: "web", - driversLocation: "drivers", - - rootURI: "/", - serviceURI: "/api/", - - productConfiguration: "conf/product.conf", - - expireSessionAfterPeriod: 1800000, - - develMode: false, - - enableSecurityManager: false, - - database: { - driver="h2_embedded_v2", - url: "jdbc:h2:${workspace}/.data/cb.h2v2.dat", - initialDataConfiguration: "conf/initial-data.conf", - pool: { - minIdleConnections: 4, - maxIdleConnections: 10, - maxConnections: 100, - validationQuery: "SELECT 1" - } - } - - }, - app: { - anonymousAccessEnabled: true, - anonymousUserRole: "user", - grantConnectionsAccessToAnonymousTeam: false, - supportsCustomConnections: false, - showReadOnlyConnectionInfo: false, - - forwardProxy: false, - - publicCredentialsSaveEnabled: true, - adminCredentialsSaveEnabled: true, - - resourceManagerEnabled: true, - - resourceQuotas: { - dataExportFileSizeLimit: 10000000, - resourceManagerFileSizeLimit: 500000, - sqlMaxRunningQueries: 100, - sqlResultSetRowsLimit: 100000, - sqlResultSetMemoryLimit: 2000000, - sqlTextPreviewMaxLength: 4096, - sqlBinaryPreviewMaxLength: 261120 - }, - enabledAuthProviders: [ - "local" - ], - - disabledDrivers: [ - "sqlite:sqlite_jdbc", - "h2:h2_embedded", - "h2:h2_embedded_v2", - "clickhouse:yandex_clickhouse" - ] - - } - -} diff --git a/config/sample-databases/README.md b/config/sample-databases/README.md deleted file mode 100644 index a6fe87c936..0000000000 --- a/config/sample-databases/README.md +++ /dev/null @@ -1,3 +0,0 @@ -### Sample databases - -Provides access to locally deployed SQLite sample database diff --git a/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/data-sources.json b/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/data-sources.json deleted file mode 100644 index 27dbbe907a..0000000000 --- a/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/data-sources.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "folders": {}, - "connections": { - "sqlite_xerial-sample-database": { - "provider": "generic", - "driver": "sqlite_jdbc", - "name": "SQLite - Chinook (Sample)", - "save-password": true, - "navigator-show-only-entities": false, - "navigator-hide-folders": false, - "read-only": false, - "template": false, - "configuration": { - "database": "${application.path}/../samples/db/Chinook.sqlitedb", - "type": "dev", - "auth-model": "native" - } - }, - "postgresql-template-1": { - "provider": "postgresql", - "driver": "postgres-jdbc", - "name": "PostgreSQL (Template)", - "save-password": false, - "show-system-objects": false, - "read-only": true, - "template": true, - "configuration": { - "host": "localhost", - "port": "5432", - "database": "postgres", - "url": "jdbc:postgresql://localhost:5432/postgres", - "type": "dev", - "provider-properties": { - "@dbeaver-show-non-default-db@": "false" - } - } - } - } -} diff --git a/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/provided-connections.json b/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/provided-connections.json deleted file mode 100644 index edc5802a0a..0000000000 --- a/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/provided-connections.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "folders": {}, - "connections": {} -} diff --git a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf b/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf deleted file mode 100644 index 67427c38c1..0000000000 --- a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf +++ /dev/null @@ -1,68 +0,0 @@ -{ - server: { - serverPort: 8978, - - workspaceLocation: "workspace", - contentRoot: "web", - driversLocation: "drivers", - - rootURI: "/", - serviceURI: "/api/", - - productConfiguration: "conf/product.conf", - - expireSessionAfterPeriod: 1800000, - - develMode: false, - - enableSecurityManager: false, - - database: { - driver="h2_embedded_v2", - url: "jdbc:h2:${workspace}/.data/cb.h2v2.dat", - initialDataConfiguration: "conf/initial-data.conf", - pool: { - minIdleConnections: 4, - maxIdleConnections: 10, - maxConnections: 100, - validationQuery: "SELECT 1" - } - } - - }, - app: { - anonymousAccessEnabled: true, - anonymousUserRole: "user", - grantConnectionsAccessToAnonymousTeam: false, - supportsCustomConnections: false, - showReadOnlyConnectionInfo: false, - - forwardProxy: false, - - publicCredentialsSaveEnabled: true, - adminCredentialsSaveEnabled: true, - - resourceManagerEnabled: true, - - resourceQuotas: { - dataExportFileSizeLimit: 10000000, - resourceManagerFileSizeLimit: 500000, - sqlMaxRunningQueries: 100, - sqlResultSetRowsLimit: 100000, - sqlResultSetMemoryLimit: 2000000, - sqlTextPreviewMaxLength: 4096, - sqlBinaryPreviewMaxLength: 261120 - }, - enabledAuthProviders: [ - "local" - ], - - disabledDrivers: [ - "h2:h2_embedded", - "h2:h2_embedded_v2", - "clickhouse:yandex_clickhouse" - ] - - } - -} diff --git a/config/sample-databases/db/Chinook.sqlitedb b/config/sample-databases/db/Chinook.sqlitedb deleted file mode 100644 index 7eb421570e..0000000000 Binary files a/config/sample-databases/db/Chinook.sqlitedb and /dev/null differ diff --git a/config/sample-databases/db/README b/config/sample-databases/db/README deleted file mode 100644 index 0882204624..0000000000 --- a/config/sample-databases/db/README +++ /dev/null @@ -1,6 +0,0 @@ - Chinook Database - Version 1.3 - Script: Chinook_Sqlite.sql - Description: Creates and populates the Chinook database. - DB Server: Sqlite - Author: Luis Rocha - License: http://www.codeplex.com/ChinookDatabase/license diff --git a/deploy/build-backend.sh b/deploy/build-backend.sh new file mode 100755 index 0000000000..ba401ce61f --- /dev/null +++ b/deploy/build-backend.sh @@ -0,0 +1,45 @@ +#!/bin/bash +set -Eeo pipefail +set +u + +echo "Clone and build Cloudbeaver" + +rm -rf ./drivers +rm -rf ./cloudbeaver +mkdir ./cloudbeaver +mkdir ./cloudbeaver/server +mkdir ./cloudbeaver/conf +mkdir ./cloudbeaver/workspace + +echo "Pull cloudbeaver platform" + +cd ../.. + +echo "Pull dbeaver platform" +[ ! -d dbeaver ] && git clone --depth 1 https://github.com/dbeaver/dbeaver.git +[ ! -d dbeaver-common ] && git clone --depth 1 https://github.com/dbeaver/dbeaver-common.git +[ ! -d dbeaver-jdbc-libsql ] && git clone --depth 1 https://github.com/dbeaver/dbeaver-jdbc-libsql.git + + +cd cloudbeaver/deploy + +echo "Build CloudBeaver server" + +cd ../server/product/aggregate +mvn clean verify $MAVEN_COMMON_OPTS -Dheadless-platform +if [[ "$?" -ne 0 ]] ; then + echo 'Could not perform package'; exit $rc +fi +cd ../../../deploy + +echo "Copy server packages" + +cp -rp ../server/product/web-server/target/products/io.cloudbeaver.product/all/all/all/* ./cloudbeaver/server +cp -p ./scripts/* ./cloudbeaver +mkdir cloudbeaver/samples + +cp -rp ../config/core/* cloudbeaver/conf +cp -rp ../config/GlobalConfiguration/.dbeaver/data-sources.json cloudbeaver/conf/initial-data-sources.conf +mv drivers cloudbeaver + +echo "End of backend build" \ No newline at end of file diff --git a/deploy/build-frontend.sh b/deploy/build-frontend.sh new file mode 100755 index 0000000000..d701060354 --- /dev/null +++ b/deploy/build-frontend.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -Eeuo pipefail + +echo "Build static content" + +mkdir -p ./cloudbeaver/web + +cd ../../cloudbeaver/webapp + +yarn install --immutable +cd ./packages/product-default +yarn run bundle + +if [[ "$?" -ne 0 ]] ; then + echo 'Application build failed'; exit $rc +fi + +cd ../../ +yarn test + +if [[ "$?" -ne 0 ]] ; then + echo 'Frontend tests failed'; exit $rc +fi + +cd ../deploy + +echo "Copy static content" + +cp -rp ../webapp/packages/product-default/lib/* cloudbeaver/web + +echo "Cloudbeaver is ready. Run run-server.sh in cloudbeaver folder to start the server." diff --git a/deploy/build-sqlite.bat b/deploy/build-sqlite.bat deleted file mode 100644 index 16e1c95963..0000000000 --- a/deploy/build-sqlite.bat +++ /dev/null @@ -1 +0,0 @@ -@call build.bat ..\config\sample-databases\SQLiteConfiguration ..\config\sample-databases\db diff --git a/deploy/build-sqlite.sh b/deploy/build-sqlite.sh deleted file mode 100755 index de4c461c3e..0000000000 --- a/deploy/build-sqlite.sh +++ /dev/null @@ -1 +0,0 @@ -./build.sh '../config/sample-databases/SQLiteConfiguration' '../config/sample-databases/db' \ No newline at end of file diff --git a/deploy/build.bat b/deploy/build.bat index f31127aa77..940c64f6da 100644 --- a/deploy/build.bat +++ b/deploy/build.bat @@ -1,11 +1,6 @@ @echo off rem command line arguments -SET CONFIGURATION_PATH=%1 -SET SAMPLE_DATABASE_PATH=%2 - -IF "%CONFIGURATION_PATH%"=="" SET CONFIGURATION_PATH="..\config\sample-databases\DefaultConfiguration" -echo "Configuration path=%CONFIGURATION_PATH%" echo Clone and build Cloudbeaver @@ -24,6 +19,9 @@ cd ..\.. echo Pull dbeaver platform IF NOT EXIST dbeaver git clone https://github.com/dbeaver/dbeaver.git +IF NOT EXIST dbeaver-common git clone https://github.com/dbeaver/dbeaver-common.git +IF NOT EXIST dbeaver-jdbc-libsql git clone https://github.com/dbeaver/dbeaver-jdbc-libsql.git + cd cloudbeaver\deploy echo Build cloudbeaver server @@ -39,32 +37,41 @@ xcopy /E /Q ..\server\product\web-server\target\products\io.cloudbeaver.product\ copy scripts\* cloudbeaver >NUL mkdir cloudbeaver\samples -IF NOT "%SAMPLE_DATABASE_PATH%"=="" ( - mkdir cloudbeaver\samples\db - xcopy /E /Q %SAMPLE_DATABASE_PATH% cloudbeaver\samples\db >NUL -) + copy ..\config\core\* cloudbeaver\conf >NUL -copy %CONFIGURATION_PATH%\GlobalConfiguration\.dbeaver\data-sources.json cloudbeaver\conf\initial-data-sources.conf >NUL -copy %CONFIGURATION_PATH%\*.conf cloudbeaver\conf >NUL +copy ..\config\DefaultConfiguration\GlobalConfiguration\.dbeaver\data-sources.json cloudbeaver\conf\initial-data-sources.conf >NUL move drivers cloudbeaver >NUL -echo Build static content +echo "Build static content" -cd ..\ +mkdir .\cloudbeaver\web -cd ..\cloudbeaver\webapp +cd ..\webapp call yarn -call yarn lerna bootstrap -call yarn lerna run build --no-bail --stream --scope=@cloudbeaver/product-default &::-- -- --env source-map +cd .\packages\product-default +call yarn run bundle + +if %ERRORLEVEL% neq 0 ( + echo 'Application build failed' + exit /b %ERRORLEVEL% +) + +cd ..\..\ +call yarn test + +if %ERRORLEVEL% neq 0 ( + echo 'Frontend tests failed' + exit /b %ERRORLEVEL% +) cd ..\deploy -echo Copy static content +echo "Copy static content" xcopy /E /Q ..\webapp\packages\product-default\lib cloudbeaver\web >NUL -echo Cloudbeaver is ready. Run run-server.bat in cloudbeaver folder to start the server. +echo "Cloudbeaver is ready. Run run-server.bat in cloudbeaver folder to start the server." pause diff --git a/deploy/build.sh b/deploy/build.sh index c259dda7a2..c3e481a4eb 100755 --- a/deploy/build.sh +++ b/deploy/build.sh @@ -1,71 +1,5 @@ #!/bin/bash set -Eeuo pipefail -#command line arguments -CONFIGURATION_PATH=${1-"../config/sample-databases/DefaultConfiguration"} -SAMPLE_DATABASE_PATH=${2-""} - -echo "Clone and build Cloudbeaver" - -rm -rf ./drivers -rm -rf ./cloudbeaver -mkdir ./cloudbeaver -mkdir ./cloudbeaver/server -mkdir ./cloudbeaver/conf -mkdir ./cloudbeaver/workspace -mkdir ./cloudbeaver/web - -echo "Pull cloudbeaver platform" - -cd ../.. - -echo "Pull dbeaver platform" -[ ! -d dbeaver ] && git clone https://github.com/dbeaver/dbeaver.git - -cd cloudbeaver/deploy - -echo "Build CloudBeaver server" - -cd ../server/product/aggregate -mvn clean verify -U -Dheadless-platform -if [[ "$?" -ne 0 ]] ; then - echo 'Could not perform package'; exit $rc -fi -cd ../../../deploy - -echo "Copy server packages" - -cp -rp ../server/product/web-server/target/products/io.cloudbeaver.product/all/all/all/* ./cloudbeaver/server -cp -p ./scripts/* ./cloudbeaver -mkdir cloudbeaver/samples - -if [[ ! -z "${SAMPLE_DATABASE_PATH}" ]]; then - mkdir cloudbeaver/samples/db - cp -rp "${SAMPLE_DATABASE_PATH}" cloudbeaver/samples/ -fi - -cp -rp ../config/core/* cloudbeaver/conf -cp -rp "${CONFIGURATION_PATH}"/GlobalConfiguration/.dbeaver/data-sources.json cloudbeaver/conf/initial-data-sources.conf -cp -p "${CONFIGURATION_PATH}"/*.conf cloudbeaver/conf/ -mv drivers cloudbeaver - -echo "Build static content" - -cd ../ - -cd ../cloudbeaver/webapp - -yarn -yarn lerna run bootstrap -yarn lerna run build --no-bail --stream --scope=@cloudbeaver/product-default #-- -- --env source-map -if [[ "$?" -ne 0 ]] ; then - echo 'Application build failed'; exit $rc -fi - -cd ../deploy - -echo "Copy static content" - -cp -rp ../webapp/packages/product-default/lib/* cloudbeaver/web - -echo "Cloudbeaver is ready. Run run-server.bat in cloudbeaver folder to start the server." +source build-backend.sh +source build-frontend.sh \ No newline at end of file diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile deleted file mode 100644 index cc14b11663..0000000000 --- a/deploy/docker/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM eclipse-temurin:17.0.4.1_1-jre-focal - -COPY cloudbeaver /opt/cloudbeaver - -EXPOSE 8978 -RUN find /opt/cloudbeaver -type d -exec chmod 775 {} \; -WORKDIR /opt/cloudbeaver/ -ENTRYPOINT ["./run-server.sh"] diff --git a/deploy/docker/base-java/Dockerfile b/deploy/docker/base-java/Dockerfile new file mode 100644 index 0000000000..d98552ddbd --- /dev/null +++ b/deploy/docker/base-java/Dockerfile @@ -0,0 +1,46 @@ +FROM ubuntu:24.04 + +MAINTAINER DBeaver Corp, devops@dbeaver.com + +ENV DEBIAN_FRONTEND=noninteractive + +RUN set -eux; \ + apt-get update && \ + apt-get dist-upgrade -y --no-install-recommends && \ + apt-get install -y --no-install-recommends \ + # curl required for historical reasons, see https://github.com/adoptium/containers/issues/255 + curl \ + # java.lang.UnsatisfiedLinkError: libfontmanager.so: libfreetype.so.6: cannot open shared object file: No such file or directory + # java.lang.NoClassDefFoundError: Could not initialize class sun.awt.X11FontManager + # https://github.com/docker-library/openjdk/pull/235#issuecomment-424466077 + fontconfig \ + # utilities for keeping Ubuntu and OpenJDK CA certificates in sync + # https://github.com/adoptium/containers/issues/293 + ca-certificates p11-kit \ + # jlink --strip-debug on 13+ needs objcopy: https://github.com/docker-library/openjdk/issues/351 + # Error: java.io.IOException: Cannot run program "objcopy": error=2, No such file or directory + binutils \ + tzdata \ + # locales ensures proper character encoding and locale-specific behaviors using en_US.UTF-8 + locales \ + nano && \ + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \ + locale-gen en_US.UTF-8; \ + rm -rf /var/lib/apt/lists/* + +ENV LANG=en_US.UTF-8 + +ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:21 $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" + +RUN set -eux; \ + # https://github.com/docker-library/openjdk/issues/331#issuecomment-498834472 + find "$JAVA_HOME/lib" -name '*.so' -exec dirname '{}' ';' | sort -u > /etc/ld.so.conf.d/docker-openjdk.conf; \ + ldconfig; \ + # https://github.com/docker-library/openjdk/issues/212#issuecomment-420979840 + # https://openjdk.java.net/jeps/341 + java -Xshare:dump; + +### Patch java security +COPY java.security* ${JAVA_HOME}/conf/security/java.security diff --git a/deploy/docker/cloudbeaver-ce/Dockerfile b/deploy/docker/cloudbeaver-ce/Dockerfile new file mode 100644 index 0000000000..6ff09b7590 --- /dev/null +++ b/deploy/docker/cloudbeaver-ce/Dockerfile @@ -0,0 +1,21 @@ +ARG BASE_JAVA_TAG="stable" +FROM dbeaver/base-java:${BASE_JAVA_TAG} + +MAINTAINER DBeaver Corp, devops@dbeaver.com + +ENV DBEAVER_GID=8978 +ENV DBEAVER_UID=8978 + +RUN groupadd -g $DBEAVER_GID dbeaver && \ + useradd -g $DBEAVER_GID -m -u $DBEAVER_UID -s /bin/bash dbeaver + +COPY cloudbeaver /opt/cloudbeaver +COPY scripts/launch-product.sh /opt/cloudbeaver/launch-product.sh + +EXPOSE 8978 +RUN find /opt/cloudbeaver -type d -exec chmod 775 {} \; +WORKDIR /opt/cloudbeaver/ + +RUN chmod +x "run-server.sh" "/opt/cloudbeaver/launch-product.sh" + +ENTRYPOINT ["./launch-product.sh"] diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/cloudbeaver-ce/docker-compose.yml similarity index 100% rename from deploy/docker/docker-compose.yml rename to deploy/docker/cloudbeaver-ce/docker-compose.yml diff --git a/deploy/docker/make-docker-container.sh b/deploy/docker/make-docker-container.sh index 17f869b3e4..f38c98d2a1 100755 --- a/deploy/docker/make-docker-container.sh +++ b/deploy/docker/make-docker-container.sh @@ -1,3 +1,3 @@ cd .. -docker build -t dbeaver/cloudbeaver:dev . --file ./docker/Dockerfile +docker build -t dbeaver/cloudbeaver:dev . --file ./docker/cloudbeaver-ce/Dockerfile diff --git a/deploy/scripts/launch-product.sh b/deploy/scripts/launch-product.sh new file mode 100644 index 0000000000..3dc4e279b6 --- /dev/null +++ b/deploy/scripts/launch-product.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# This script is needed to change ownership and run the application as user dbeaver during the upgrade from version 24.2.0 + +# Change ownership of the WORKDIR to the dbeaver user and group +# Variables DBEAVER_ are defined in the Dockerfile and exported to the runtime environment +# PWD equals WORKDIR value from product Dockerfile + +if [ "$(id -u)" -eq 0 ]; then + TARGET_USER=${TARGET_USER:-dbeaver} + TARGET_UID=${TARGET_UID:-$DBEAVER_UID} + TARGET_GID=${TARGET_GID:-$DBEAVER_GID} + + chown -R $DBEAVER_UID:$DBEAVER_GID $PWD/workspace + # Execute run-server.sh as the dbeaver user with the JAVA_HOME and PATH environment variables + exec su "$TARGET_USER" -c "JAVA_HOME=$JAVA_HOME PATH=$PATH ./run-server.sh" +else + exec ./run-server.sh +fi \ No newline at end of file diff --git a/deploy/scripts/run-server.bat b/deploy/scripts/run-server.bat index 9989e947e0..32367aa93c 100644 --- a/deploy/scripts/run-server.bat +++ b/deploy/scripts/run-server.bat @@ -1,5 +1,5 @@ @echo off -for /f %%a in ('dir /B /S server\plugins\org.eclipse.equinox.launcher*.jar') do SET launcherJar="%%a" +for /f %%a in ('dir /B /S server\plugins\org.jkiss.dbeaver.launcher*.jar') do SET launcherJar="%%a" echo "Starting Cloudbeaver Server" @@ -10,4 +10,38 @@ 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 +SET VMARGS_OPTS = "" +If Not Defined JAVA_OPTS ( + SET VMARGS_OPTS = "-Xmx2048M" +) + +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 ^ + --add-opens=java.sql/java.sql=ALL-UNNAMED ^ + -jar %launcherJar% ^ + -product io.cloudbeaver.product.ce.product ^ + -data %workspacePath% ^ + -web-config conf/cloudbeaver.conf ^ + -nl en ^ + -registryMultiLanguage diff --git a/deploy/scripts/run-server.sh b/deploy/scripts/run-server.sh index c15d98983a..8b67c5ca96 100755 --- a/deploy/scripts/run-server.sh +++ b/deploy/scripts/run-server.sh @@ -1,9 +1,41 @@ #!/bin/bash -launcherJar=( server/plugins/org.eclipse.equinox.launcher*.jar ) +launcherJar=( server/plugins/org.jkiss.dbeaver.launcher*.jar ) 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 +[ ! -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 +exec 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 \ + --add-opens=java.sql/java.sql=ALL-UNNAMED \ + -jar ${launcherJar} \ + -product io.cloudbeaver.product.ce.product \ + -web-config conf/cloudbeaver.conf \ + ${CLI_OPTS} \ + -nl en \ + -registryMultiLanguage diff --git a/deploy/scripts/server-cli.sh b/deploy/scripts/server-cli.sh new file mode 100755 index 0000000000..8738ca3638 --- /dev/null +++ b/deploy/scripts/server-cli.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -Eeo pipefail +CLI_OPTS="$@" +if [[ $# -eq 0 ]] ; + then + CLI_OPTS='-help' +fi +source run-server.sh \ No newline at end of file diff --git a/deploy/supervisor/cloudbeaver.conf b/deploy/supervisor/cloudbeaver.conf deleted file mode 100644 index 01d38086f8..0000000000 --- a/deploy/supervisor/cloudbeaver.conf +++ /dev/null @@ -1,10 +0,0 @@ -[program:cloudbeaver] -directory=/opt/cloudbeaver/ -command=/opt/cloudbeaver/run-server.sh -stopasgroup=true -killasgroup=true -stopsignal=INT -autostart=true -autorestart=true -redirect_stderr=true -stdout_logfile=/var/log/cloudbeaver-server.log diff --git a/generate_workspace.cmd b/generate_workspace.cmd new file mode 100644 index 0000000000..ea0d989b96 --- /dev/null +++ b/generate_workspace.cmd @@ -0,0 +1,2 @@ +@echo off +%~dp0\..\idea-rcp-launch-config-generator\runGenerator.cmd -f %~dp0 \ No newline at end of file diff --git a/generate_workspace.sh b/generate_workspace.sh new file mode 100755 index 0000000000..f7ac4329ab --- /dev/null +++ b/generate_workspace.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +script_dir="$(realpath "$(dirname "$0")")" +repositories_root_dir="$(realpath "$script_dir/..")" + +"$repositories_root_dir"/idea-rcp-launch-config-generator/runGenerator.sh "$script_dir" diff --git a/javaConfig.json b/javaConfig.json deleted file mode 100644 index 081953fccf..0000000000 --- a/javaConfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "projects": ["../dbeaver", "../cloudbeaver/server"], - "targetPlatform": "./org.jkiss.cloudbeaver.tp.target" -} diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000000..eee991d5c5 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,15 @@ +lefthook: | + cd webapp + yarn dlx --quiet lefthook +output: false +pre-commit: + commands: + check-license: + skip: + - merge + - rebase + root: webapp + glob: "*.{ts,tsx}" + exclude: + - "**/locales/**" + run: yarn core-cli-check-license {staged_files} diff --git a/org.jkiss.cloudbeaver.tp.target b/org.jkiss.cloudbeaver.tp.target deleted file mode 100644 index 9a7482ada6..0000000000 --- a/org.jkiss.cloudbeaver.tp.target +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/osgi-app.properties b/osgi-app.properties new file mode 100644 index 0000000000..e68150aaeb --- /dev/null +++ b/osgi-app.properties @@ -0,0 +1,37 @@ +workspaceName=cloudbeaver-ce +featuresPaths=\ + dbeaver/features;\ + cloudbeaver/server/features; +bundlesPaths=\ + dbeaver-common/modules;\ + dbeaver/plugins;\ + cloudbeaver/server/bundles;\ + dbeaver-jdbc-libsql;\ + cloudbeaver/server/test +testLibraries=\ + org.junit;\ + org.mockito.mockito-core;\ + junit-jupiter-api;\ + org.opentest4j;\ + org.hamcrest.core;\ + org.hamcrest +repositories=\ + https://repo.dbeaver.net/p2/ce/latest/;\ + https://download.eclipse.org/releases/${eclipse-version}/; +testBundles=\ + org.junit;\ + org.mockito.mockito-core;\ + junit-jupiter-api;\ + org.opentest4j +productsPaths=\ + dbeaver/product/community/DBeaver.product;\ + cloudbeaver/server/product/web-server/CloudbeaverServer.product:../../opt/cloudbeaver;\ + cloudbeaver/server/product/web-server-test/CloudbeaverServerUnitTest.product:../../opt/cloudbeaver; +ideaConfigurationFilesPaths=\ + dbeaver/.ide/.idea; +additionalModuleRoots=\ + opt; +optionalFeatureRepositories=\ + dbeaver/product/repositories +excludeOutputs=\ + cloudbeaver/webapp/node_modules \ No newline at end of file diff --git a/project.deps b/project.deps new file mode 100644 index 0000000000..2c6c189ff9 --- /dev/null +++ b/project.deps @@ -0,0 +1,3 @@ +dbeaver-common +dbeaver-jdbc-libsql +dbeaver \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index 18cf875097..e9751b2613 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -3,42 +3,51 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Model Bundle-SymbolicName: io.cloudbeaver.model;singleton:=true -Bundle-Version: 1.0.38.qualifier -Bundle-Release-Date: 20230209 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 1.0.85.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy -Bundle-ClassPath: . -Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport, +Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport, org.jkiss.dbeaver.model;visibility:=reexport, + org.jkiss.dbeaver.model.cli;visibility:=reexport, org.jkiss.dbeaver.model.sm;visibility:=reexport, org.jkiss.dbeaver.model.event;visibility:=reexport, org.jkiss.dbeaver.model.nio;visibility:=reexport, org.jkiss.dbeaver.registry;visibility:=reexport, org.jkiss.dbeaver.net.ssh, - org.jkiss.bundle.graphql.java;visibility:=reexport, + org.jkiss.bundle.graphql;visibility:=reexport, org.jkiss.bundle.apache.dbcp, com.google.gson;visibility:=reexport, - jakarta.servlet-api;visibility:=reexport + jakarta.servlet-api;bundle-version:="6.0.0";visibility:=reexport Export-Package: io.cloudbeaver, io.cloudbeaver.auth, io.cloudbeaver.auth.provider, + io.cloudbeaver.auth.provider.fa, io.cloudbeaver.auth.provider.local, io.cloudbeaver.auth.provisioning, io.cloudbeaver.websocket, io.cloudbeaver.model, io.cloudbeaver.model.app, + io.cloudbeaver.model.config, + io.cloudbeaver.model.apilog, + io.cloudbeaver.model.cli, + io.cloudbeaver.model.events, + io.cloudbeaver.model.fs, + io.cloudbeaver.model.log, io.cloudbeaver.model.rm, io.cloudbeaver.model.rm.local, io.cloudbeaver.model.rm.lock, - io.cloudbeaver.model.rm.fs.nio, io.cloudbeaver.model.session, io.cloudbeaver.model.user, + io.cloudbeaver.model.utils, io.cloudbeaver.registry, io.cloudbeaver.server, + io.cloudbeaver.server.filters, io.cloudbeaver.service, io.cloudbeaver.service.admin, io.cloudbeaver.service.security, io.cloudbeaver.service.sql, + io.cloudbeaver.test, io.cloudbeaver.utils, io.cloudbeaver.utils.file Automatic-Module-Name: io.cloudbeaver.model diff --git a/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle.properties b/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle.properties new file mode 100644 index 0000000000..bcfc808dd6 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle.properties @@ -0,0 +1,19 @@ +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.osInfo.name=OS +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.memoryAvailable.name=Memory available +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.javaVersion.name=Java version +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.javaParameters.name=JVM parameters +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.productName.name=Product name +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.productVersion.name=Product version +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.workspacePath.name=Workspace path +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.installPath.name=Install path +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.deploymentType.name=Deployment type +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.smDbUrl.name=Security manager database URL +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.smDbDriverName.name=Security manager database driver name +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.smDbProductName.name=Security manager database product name +meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.smDbProductVersion.name=Security manager database product version + +meta.io.cloudbeaver.model.WebExpertSettingsProperties.readOnly.name = Read-only connection +meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = Auto-commit +meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = Keep-alive interval (seconds) +meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultSchema.name = Default schema +meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultCatalog.name = Default catalog \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_fr.properties b/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_fr.properties new file mode 100644 index 0000000000..c03222eb0b --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_fr.properties @@ -0,0 +1,2 @@ +meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = Garder actif (en secondes) +meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.description = Pas de déconnexion automatique \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_ru.properties b/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_ru.properties new file mode 100644 index 0000000000..08a1d65e20 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_ru.properties @@ -0,0 +1,5 @@ +meta.io.cloudbeaver.model.WebExpertSettingsProperties.readOnly.name = Только для чтения +meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = Авто-коммит +meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = Интервал Keep-alive (в секундах) +meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultSchema.name = Схема по умолчанию +meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultCatalog.name = Каталог по умолчанию \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_vi.properties b/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_vi.properties new file mode 100644 index 0000000000..f60d3e3372 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_vi.properties @@ -0,0 +1,4 @@ +meta.io.cloudbeaver.model.WebExpertSettingsProperties.readOnly.name = Kết nối chỉ đọc +meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = Tự động commit +meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = Giữ kết nối (tính bằng giây) +meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.description = Không tự động ngắt kết nối \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_zh.properties b/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_zh.properties new file mode 100644 index 0000000000..662a0c1b41 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/OSGI-INF/l10n/bundle_zh.properties @@ -0,0 +1,3 @@ +meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = 自动提交 +meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = 保持连接 (秒) +meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.description = 无自动断开连接 \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/build.properties b/server/bundles/io.cloudbeaver.model/build.properties index d9cc939477..436dfbcddc 100644 --- a/server/bundles/io.cloudbeaver.model/build.properties +++ b/server/bundles/io.cloudbeaver.model/build.properties @@ -2,5 +2,6 @@ source.. = src/ output.. = target/classes/ bin.includes = .,\ META-INF/,\ + OSGI-INF/,\ plugin.xml,\ schema/ diff --git a/server/bundles/io.cloudbeaver.model/plugin.xml b/server/bundles/io.cloudbeaver.model/plugin.xml index 196b88bb35..f503391bb6 100644 --- a/server/bundles/io.cloudbeaver.model/plugin.xml +++ b/server/bundles/io.cloudbeaver.model/plugin.xml @@ -4,6 +4,7 @@ + @@ -19,4 +20,9 @@ + + + + diff --git a/server/bundles/io.cloudbeaver.model/pom.xml b/server/bundles/io.cloudbeaver.model/pom.xml index 30f8b3a4f4..cf4794d20f 100644 --- a/server/bundles/io.cloudbeaver.model/pom.xml +++ b/server/bundles/io.cloudbeaver.model/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.model - 1.0.38-SNAPSHOT + 1.0.85-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.metaParameters.exsd b/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.metaParameters.exsd index fe9b6f92fb..0dd860a608 100644 --- a/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.metaParameters.exsd +++ b/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.metaParameters.exsd @@ -14,11 +14,6 @@ - - - - - diff --git a/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.server.feature.exsd b/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.server.feature.exsd new file mode 100644 index 0000000000..93c93336c8 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.server.feature.exsd @@ -0,0 +1,34 @@ + + + + + + + + Web feature + + + + + + + + + + + + + + + + + + + + Web service description + + + + + + 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..6e6a276d0e 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,23 +16,21 @@ */ package io.cloudbeaver; -import org.eclipse.core.resources.IProject; import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMSessionContext; +import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMControllerProvider; import org.jkiss.dbeaver.model.rm.RMProject; -import org.jkiss.dbeaver.model.rm.RMUtils; -import org.jkiss.dbeaver.registry.BaseProjectImpl; +import org.jkiss.dbeaver.model.rm.RMProjectType; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.Pair; import java.nio.file.Path; import java.util.Collection; -public class BaseWebProjectImpl extends BaseProjectImpl implements RMControllerProvider { +public abstract class BaseWebProjectImpl extends BaseProjectImpl implements RMControllerProvider { @NotNull private final RMProject project; @@ -40,7 +38,6 @@ public class BaseWebProjectImpl extends BaseProjectImpl implements RMControllerP @NotNull private final Path path; @NotNull - protected final DataSourceFilter dataSourceFilter; private final RMController resourceController; public BaseWebProjectImpl( @@ -48,13 +45,12 @@ public BaseWebProjectImpl( @NotNull RMController resourceController, @NotNull SMSessionContext sessionContext, @NotNull RMProject project, - @NotNull DataSourceFilter dataSourceFilter + @NotNull Path path ) { super(workspace, sessionContext); this.resourceController = resourceController; - this.path = RMUtils.getProjectPath(project); + this.path = path; this.project = project; - this.dataSourceFilter = dataSourceFilter; } @NotNull @@ -62,6 +58,12 @@ public RMController getResourceController() { return resourceController; } + @NotNull + @Override + public RMProject getRMProject() { + return project; + } + @Override public boolean isVirtual() { return true; @@ -85,12 +87,6 @@ public Path getAbsolutePath() { return path; } - @Nullable - @Override - public IProject getEclipseProject() { - return null; - } - @Override public boolean isOpen() { return true; @@ -106,11 +102,6 @@ public boolean isUseSecretStorage() { return false; } - @NotNull - public RMProject getRmProject() { - return this.project; - } - /** * Method for Bulk Update of resources properties paths * @@ -154,4 +145,9 @@ public boolean resetResourcesPropertiesBatch(@NotNull Collection resourc public Path getMetadataFilePath() { return getMetadataPath().resolve(METADATA_STORAGE_FILE); } + + @Override + public boolean isPrivateProject() { + return RMProjectType.USER.equals(getRMProject().getType()); + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWConstants.java index 5a83a18ead..d8987fa519 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWConstants.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -22,23 +22,31 @@ /** * General constants */ -public class DBWConstants { +public interface DBWConstants { - public static final String PERMISSION_ADMIN = DBAPermissionRealm.PERMISSION_ADMIN; + String PERMISSION_ADMIN = DBAPermissionRealm.PERMISSION_ADMIN; - public static final String PERMISSION_CONFIGURATION_MANAGER = RMConstants.PERMISSION_CONFIGURATION_MANAGER; - public static final String PERMISSION_PRIVATE_PROJECT_ACCESS = "private-project-access"; + String PERMISSION_CONFIGURATION_MANAGER = RMConstants.PERMISSION_CONFIGURATION_MANAGER; + String PERMISSION_PRIVATE_PROJECT_ACCESS = "private-project-access"; + String PERMISSION_SECRET_MANAGER = "secret-manager"; + String PERMISSION_SQL_RESULT_UPDATE = "sql-result-update"; + String PERMISSION_SQL_EXECUTE_QUERY = "sql-execute-query"; - public static final String PERMISSION_EDIT_STRUCTURE = "edit-meta"; - public static final String PERMISSION_EDIT_DATA = "edit-data"; + String GLOBAL_PERMISSION_SCRIPT_EXECUTE = "permission.sql.script.execution"; + String GLOBAL_PERMISSION_DATA_EDITOR_IMPORT = "permission.data-editor.import"; + String GLOBAL_PERMISSION_DATA_EDITOR_EDITING = "permission.data-editor.editing"; - public static final String STATE_ATTR_SIGN_IN_STATE = "state.signin"; + String PERMISSION_EDIT_STRUCTURE = "edit-meta"; + String PERMISSION_EDIT_DATA = "edit-data"; - public enum SignInState { + String STATE_ATTR_SIGN_IN_STATE = "state.signin"; + String WORK_DATA_FOLDER_NAME = ".work-data"; + + enum SignInState { GLOBAL, EMBEDDED } - + String TASK_STATUS_FINISHED = "Finished"; //public static final String PERMISSION_USER = "user"; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWFeatureSet.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWFeatureSet.java index df18e4dd65..af07462401 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWFeatureSet.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWFeatureSet.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -36,4 +36,6 @@ public interface DBWFeatureSet { boolean isEnabled(); + boolean isEnabledByDefault(); + } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWUserIdentity.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWUserIdentity.java index c5f1fd39ad..a7cc2029c4 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWUserIdentity.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWUserIdentity.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWebException.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWebException.java index 6d43d9e339..a06626ed51 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWebException.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWebException.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -21,8 +21,7 @@ import graphql.GraphQLError; import graphql.language.SourceLocation; import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.DBPDataSource; -import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.dbeaver.model.sql.SQLState; import org.jkiss.utils.CommonUtils; import java.io.PrintWriter; @@ -38,6 +37,7 @@ public class DBWebException extends DBException implements GraphQLError { public static final String ERROR_CODE_SESSION_EXPIRED = "sessionExpired"; public static final String ERROR_CODE_ACCESS_DENIED = "accessDenied"; + public static final String ERROR_CODE_SERVER_NOT_INITIALIZED = "serverNotInitialized"; public static final String ERROR_CODE_LICENSE_DENIED = "licenseRequired"; public static final String ERROR_CODE_IDENT_REQUIRED = "identRequired"; public static final String ERROR_CODE_AUTH_REQUIRED = "authRequired"; @@ -65,12 +65,8 @@ public DBWebException(String message, String errorCode, Throwable cause) { this.webErrorCode = errorCode; } - public DBWebException(Throwable cause, DBPDataSource dataSource) { - super(cause, dataSource); - } - - public DBWebException(String message, Throwable cause, DBPDataSource dataSource) { - super(makeMessage(message, cause), cause, dataSource); + public DBWebException(Throwable cause) { + super(null, cause); } public String getWebErrorCode() { @@ -103,7 +99,7 @@ public ErrorClassification getErrorType() { @Override public Map getExtensions() { StringWriter buf = new StringWriter(); - GeneralUtils.getRootCause(this).printStackTrace(new PrintWriter(buf, true)); + CommonUtils.getRootCause(this).printStackTrace(new PrintWriter(buf, true)); Map extensions = new LinkedHashMap<>(); String stString = buf.toString(); @@ -128,14 +124,14 @@ public Map getExtensions() { //stString = stString.substring(divPos + 1).trim(); } extensions.put("stackTrace", stString.trim()); - int errorCode = getErrorCode(); + int errorCode = SQLState.getCodeFromException(this); if (errorCode != ERROR_CODE_NONE) { extensions.put("errorCode", errorCode); } if (!CommonUtils.isEmpty(webErrorCode)) { extensions.put("webErrorCode", webErrorCode); } - String databaseState = getDatabaseState(); + String databaseState = SQLState.getStateFromException(this); if (databaseState != null) { extensions.put("databaseState", databaseState); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DataSourceFilter.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DataSourceFilter.java index c31dae8f91..23da538552 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DataSourceFilter.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DataSourceFilter.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceConnectEventListener.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceConnectEventListener.java new file mode 100644 index 0000000000..11b4cee2b3 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceConnectEventListener.java @@ -0,0 +1,55 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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; + +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.DBPEvent; +import org.jkiss.dbeaver.model.DBPEventListener; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceConnectEvent; + +/** + * Sends a WebSocket event when a data source is connected. + */ +public class WebDataSourceConnectEventListener implements DBPEventListener { + + @NotNull + private final WebSession webSession; + + public WebDataSourceConnectEventListener(@NotNull WebSession webSession) { + this.webSession = webSession; + } + + @Override + public void handleDataSourceEvent(DBPEvent event) { + if (!(event.getObject() instanceof DBPDataSourceContainer container)) { + return; + } + + if (event.getAction() == DBPEvent.Action.AFTER_CONNECT && event.getEnabled()) { + webSession.addSessionEvent( + new WSDataSourceConnectEvent( + container.getProject().getId(), + container.getId(), + webSession.getSessionId(), + webSession.getUserId() + ) + ); + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java deleted file mode 100644 index 072304983a..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java +++ /dev/null @@ -1,353 +0,0 @@ -/* - * 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; - -import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.*; -import org.jkiss.dbeaver.model.access.DBAAuthProfile; -import org.jkiss.dbeaver.model.access.DBACredentialsProvider; -import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; -import org.jkiss.dbeaver.model.app.DBPDataSourceRegistryCache; -import org.jkiss.dbeaver.model.app.DBPProject; -import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; -import org.jkiss.dbeaver.model.connection.DBPDriver; -import org.jkiss.dbeaver.model.net.DBWNetworkProfile; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.model.secret.DBSSecretController; -import org.jkiss.dbeaver.model.struct.DBSObjectFilter; -import org.jkiss.dbeaver.registry.DataSourceConfigurationManager; -import org.jkiss.dbeaver.registry.DataSourcePersistentRegistry; -import org.jkiss.dbeaver.registry.DataSourceRegistry; - -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -public class WebDataSourceRegistryProxy implements DBPDataSourceRegistry, DataSourcePersistentRegistry, DBPDataSourceRegistryCache { - private final DataSourceFilter dataSourceFilter; - private final DataSourceRegistry dataSourceRegistry; - - public WebDataSourceRegistryProxy(DataSourceRegistry dataSourceRegistry, DataSourceFilter filter) { - this.dataSourceRegistry = dataSourceRegistry; - this.dataSourceFilter = filter; - } - - @Override - public DBPProject getProject() { - return dataSourceRegistry.getProject(); - } - - @Nullable - @Override - public DBPDataSourceContainer getDataSource(String id) { - DBPDataSourceContainer dataSource = dataSourceRegistry.getDataSource(id); - if (dataSource == null || dataSourceFilter != null && !dataSourceFilter.filter(dataSource)) { - return null; - } - return dataSource; - } - - @Nullable - @Override - public DBPDataSourceContainer getDataSource(DBPDataSource dataSource) { - if (dataSourceFilter != null && !dataSourceFilter.filter(dataSource.getContainer())) { - return null; - } - return dataSourceRegistry.getDataSource(dataSource); - } - - @Nullable - @Override - public DBPDataSourceContainer findDataSourceByName(String name) { - var dataSource = dataSourceRegistry.findDataSourceByName(name); - if (dataSource != null) { - if (dataSourceFilter == null || dataSourceFilter.filter(dataSource)) { - return dataSource; - } - } - return null; - } - - @NotNull - @Override - public List getDataSourcesByProfile(@NotNull DBWNetworkProfile profile) { - return dataSourceRegistry.getDataSourcesByProfile(profile) - .stream() - .filter(dataSourceFilter::filter) - .collect(Collectors.toList()); - } - - @NotNull - @Override - public List getDataSources() { - return dataSourceRegistry.getDataSources() - .stream() - .filter(dataSourceFilter::filter) - .collect(Collectors.toList()); - } - - @NotNull - @Override - public DBPDataSourceContainer createDataSource(DBPDriver driver, DBPConnectionConfiguration connConfig) { - return dataSourceRegistry.createDataSource(driver, connConfig); - } - - @NotNull - @Override - public DBPDataSourceContainer createDataSource(DBPDataSourceContainer source) { - return dataSourceRegistry.createDataSource(source); - } - - @Override - public void addDataSourceListener(@NotNull DBPEventListener listener) { - dataSourceRegistry.addDataSourceListener(listener); - } - - @Override - public boolean removeDataSourceListener(@NotNull DBPEventListener listener) { - return dataSourceRegistry.removeDataSourceListener(listener); - } - - @Override - public void addDataSource(@NotNull DBPDataSourceContainer dataSource) throws DBException { - dataSourceRegistry.addDataSource(dataSource); - } - - @Override - public void removeDataSource(@NotNull DBPDataSourceContainer dataSource) { - dataSourceRegistry.removeDataSource(dataSource); - } - - @Override - public void addDataSourceToList(@NotNull DBPDataSourceContainer dataSource) { - dataSourceRegistry.addDataSourceToList(dataSource); - } - - @Override - public void removeDataSourceFromList(@NotNull DBPDataSourceContainer dataSource) { - dataSourceRegistry.removeDataSourceFromList(dataSource); - } - - @Override - public void updateDataSource(@NotNull DBPDataSourceContainer dataSource) throws DBException { - dataSourceRegistry.updateDataSource(dataSource); - } - - @NotNull - @Override - public List getAllFolders() { - return dataSourceRegistry.getAllFolders(); - } - - @NotNull - @Override - public List getRootFolders() { - return dataSourceRegistry.getRootFolders(); - } - - @Override - public DBPDataSourceFolder getFolder(String path) { - return dataSourceRegistry.getFolder(path); - } - - @Override - public DBPDataSourceFolder addFolder(DBPDataSourceFolder parent, String name) { - return dataSourceRegistry.addFolder(parent, name); - } - - @Override - public void removeFolder(DBPDataSourceFolder folder, boolean dropContents) { - dataSourceRegistry.removeFolder(folder, dropContents); - } - - @Override - public void moveFolder(@NotNull String oldPath, @NotNull String newPath) { - dataSourceRegistry.moveFolder(oldPath, newPath); - } - - @Nullable - @Override - public DBSObjectFilter getSavedFilter(String name) { - return dataSourceRegistry.getSavedFilter(name); - } - - @NotNull - @Override - public List getSavedFilters() { - return dataSourceRegistry.getSavedFilters(); - } - - @Override - public void updateSavedFilter(DBSObjectFilter filter) { - dataSourceRegistry.updateSavedFilter(filter); - } - - @Override - public void removeSavedFilter(String filterName) { - dataSourceRegistry.removeSavedFilter(filterName); - } - - @Nullable - @Override - public DBWNetworkProfile getNetworkProfile(String source, String name) { - return dataSourceRegistry.getNetworkProfile(source, name); - } - - @NotNull - @Override - public List getNetworkProfiles() { - return dataSourceRegistry.getNetworkProfiles(); - } - - @Override - public void updateNetworkProfile(DBWNetworkProfile profile) { - dataSourceRegistry.updateNetworkProfile(profile); - } - - @Override - public void removeNetworkProfile(DBWNetworkProfile profile) { - dataSourceRegistry.removeNetworkProfile(profile); - } - - @Nullable - @Override - public DBAAuthProfile getAuthProfile(String id) { - return dataSourceRegistry.getAuthProfile(id); - } - - @NotNull - @Override - public List getAllAuthProfiles() { - return dataSourceRegistry.getAllAuthProfiles(); - } - - @NotNull - @Override - public List getApplicableAuthProfiles(@Nullable DBPDriver driver) { - return dataSourceRegistry.getApplicableAuthProfiles(driver); - } - - @Override - public void updateAuthProfile(DBAAuthProfile profile) { - dataSourceRegistry.updateAuthProfile(profile); - } - - @Override - public void removeAuthProfile(DBAAuthProfile profile) { - dataSourceRegistry.removeAuthProfile(profile); - } - - @Override - public void flushConfig() { - dataSourceRegistry.flushConfig(); - } - - @Override - public void refreshConfig() { - dataSourceRegistry.refreshConfig(); - } - - @Override - public void refreshConfig(@Nullable Collection dataSourceIds) { - dataSourceRegistry.refreshConfig(dataSourceIds); - } - - @Override - public Throwable getLastError() { - return dataSourceRegistry.getLastError(); - } - - @Override - public boolean hasError() { - return dataSourceRegistry.hasError(); - } - - @Override - public void checkForErrors() throws DBException { - dataSourceRegistry.checkForErrors(); - } - - @Override - public void notifyDataSourceListeners(DBPEvent event) { - dataSourceRegistry.notifyDataSourceListeners(event); - } - - @Nullable - @Override - public DBACredentialsProvider getAuthCredentialsProvider() { - return dataSourceRegistry.getAuthCredentialsProvider(); - } - - @Override - public void dispose() { - dataSourceRegistry.dispose(); - } - - @Override - public void setAuthCredentialsProvider(DBACredentialsProvider authCredentialsProvider) { - dataSourceRegistry.setAuthCredentialsProvider(authCredentialsProvider); - } - - @Override - public Set getTemporaryFolders() { - return dataSourceRegistry.getTemporaryFolders(); - } - - @Override - public boolean loadDataSources( - @NotNull List storages, - @NotNull DataSourceConfigurationManager manager, - @Nullable Collection dataSourceIds, boolean refresh, - boolean purgeUntouched - ) { - return dataSourceRegistry.loadDataSources(storages, manager, dataSourceIds, refresh, purgeUntouched); - } - - @Override - public void saveDataSources() { - dataSourceRegistry.saveDataSources(); - } - - @Override - public DataSourceConfigurationManager getConfigurationManager() { - return dataSourceRegistry.getConfigurationManager(); - } - - @Override - public void saveConfigurationToManager( - @NotNull DBRProgressMonitor monitor, - @NotNull DataSourceConfigurationManager configurationManager, - @Nullable Predicate filter - ) { - dataSourceRegistry.saveConfigurationToManager(monitor, configurationManager, filter); - } - - @Override - public void persistSecrets(DBSSecretController secretController) throws DBException { - dataSourceRegistry.persistSecrets(secretController); - } - - @Override - public void resolveSecrets(DBSSecretController secretController) throws DBException { - dataSourceRegistry.resolveSecrets(secretController); - } -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebGlobalProjectRegistryProxy.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebGlobalProjectRegistryProxy.java new file mode 100644 index 0000000000..6eb54b4f78 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebGlobalProjectRegistryProxy.java @@ -0,0 +1,443 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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; + +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.*; +import org.jkiss.dbeaver.model.access.DBAAuthProfile; +import org.jkiss.dbeaver.model.access.DBACredentialsProvider; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistryCache; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.model.net.DBWNetworkProfile; +import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.struct.DBSObjectFilter; +import org.jkiss.dbeaver.registry.DataSourceConfigurationManager; +import org.jkiss.dbeaver.registry.DataSourceParseResults; +import org.jkiss.dbeaver.registry.DataSourcePersistentRegistry; +import org.jkiss.dbeaver.registry.DataSourceRegistry; + +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Proxy for a global project data source registry. + * We need to filter some data sources in case of inaccessibility (not enough permissions). + */ +public class WebGlobalProjectRegistryProxy implements DBPDataSourceRegistry, DataSourcePersistentRegistry, DBPDataSourceRegistryCache { + @NotNull + private final WebSession webSession; + @NotNull + private final DataSourceFilter dataSourceFilter; + @NotNull + private final DataSourceRegistry dataSourceRegistry; + + public WebGlobalProjectRegistryProxy( + @NotNull WebSession webSession, + @NotNull DataSourceRegistry dataSourceRegistry, + @NotNull DataSourceFilter filter + ) { + this.webSession = webSession; + this.dataSourceRegistry = dataSourceRegistry; + this.dataSourceFilter = filter; + } + + @NotNull + @Override + public DBPProject getProject() { + return dataSourceRegistry.getProject(); + } + + @Nullable + @Override + public DBPDataSourceContainer getDataSource(@NotNull String id) { + DBPDataSourceContainer dataSource = dataSourceRegistry.getDataSource(id); + if (dataSource == null || !dataSourceFilter.filter(dataSource)) { + return null; + } + return dataSource; + } + + @Nullable + @Override + public DBPDataSourceContainer getDataSource(@NotNull DBPDataSource dataSource) { + if (!dataSourceFilter.filter(dataSource.getContainer())) { + return null; + } + return dataSourceRegistry.getDataSource(dataSource); + } + + @Nullable + @Override + public DBPDataSourceContainer findDataSourceByName(String name) { + var dataSource = dataSourceRegistry.findDataSourceByName(name); + if (dataSource != null && dataSourceFilter.filter(dataSource)) { + return dataSource; + } + return null; + } + + @NotNull + @Override + public List getDataSourcesByProfile(@NotNull DBWNetworkProfile profile) { + return dataSourceRegistry.getDataSourcesByProfile(profile) + .stream() + .filter(dataSourceFilter::filter) + .collect(Collectors.toList()); + } + + @NotNull + @Override + public List getDataSources() { + return dataSourceRegistry.getDataSources() + .stream() + .filter(dataSourceFilter::filter) + .collect(Collectors.toList()); + } + + @NotNull + @Override + public DBPDataSourceContainer createDataSource(@NotNull DBPDriver driver, @NotNull DBPConnectionConfiguration connConfig) { + return dataSourceRegistry.createDataSource(driver, connConfig); + } + + @Override + public DBPDataSourceContainer createDataSource( + @NotNull String id, + @NotNull DBPDriver driver, + @NotNull DBPConnectionConfiguration connConfig + ) { + return dataSourceRegistry.createDataSource(id, driver, connConfig); + } + + @Override + public DBPDataSourceContainer createDataSource( + @NotNull DBPDataSourceConfigurationStorage dataSourceStorage, + @NotNull DBPDataSourceOrigin origin, + @NotNull String id, + @NotNull DBPDriver driver, + @NotNull DBPConnectionConfiguration configuration + ) { + return dataSourceRegistry.createDataSource(dataSourceStorage, origin, id, driver, configuration); + } + + @NotNull + @Override + public DBPDataSourceContainer createDataSource(@NotNull DBPDataSourceContainer source) { + return dataSourceRegistry.createDataSource(source); + } + + @Override + public void addDataSourceListener(@NotNull DBPEventListener listener) { + dataSourceRegistry.addDataSourceListener(new WebDBPEventListenerProxy(listener)); + } + + @Override + public boolean removeDataSourceListener(@NotNull DBPEventListener listener) { + return dataSourceRegistry.removeDataSourceListener(listener); + } + + @Override + public void addDataSource(@NotNull DBPDataSourceContainer dataSource) throws DBException { + dataSourceRegistry.addDataSource(dataSource); + } + + @Override + public void removeDataSource(@NotNull DBPDataSourceContainer dataSource) { + dataSourceRegistry.removeDataSource(dataSource); + } + + @Override + public void addDataSourceToList(@NotNull DBPDataSourceContainer dataSource) { + dataSourceRegistry.addDataSourceToList(dataSource); + } + + @Override + public void removeDataSourceFromList(@NotNull DBPDataSourceContainer dataSource) { + dataSourceRegistry.removeDataSourceFromList(dataSource); + } + + @Override + public void updateDataSource(@NotNull DBPDataSourceContainer dataSource) throws DBException { + dataSourceRegistry.updateDataSource(dataSource); + } + + @NotNull + @Override + public List getAllFolders() { + if (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) { + return dataSourceRegistry.getAllFolders(); + } + Set set = new LinkedHashSet<>(); + for (DBPDataSourceContainer container : getDataSources()) { + DBPDataSourceFolder folder = container.getFolder(); + while (folder != null) { + set.add(folder); + folder = folder.getParent(); + } + } + return new ArrayList<>(set); + } + + @NotNull + @Override + public List getRootFolders() { + if (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) { + return dataSourceRegistry.getRootFolders(); + } + return getDataSources().stream() + .map(DBPDataSourceContainer::getFolder) + .filter(folder -> folder != null && folder.getParent() == null) + .toList(); + } + + @NotNull + @Override + public DBPDataSourceFolder getFolder(@NotNull String path) { + return dataSourceRegistry.getFolder(path); + } + + @NotNull + @Override + public DBPDataSourceFolder addFolder(@Nullable DBPDataSourceFolder parent, @NotNull String name) { + return dataSourceRegistry.addFolder(parent, name); + } + + @Override + public void removeFolder(@NotNull DBPDataSourceFolder folder, boolean dropContents) { + dataSourceRegistry.removeFolder(folder, dropContents); + } + + @Override + public void moveFolder(@NotNull String oldPath, @NotNull String newPath) throws DBException { + dataSourceRegistry.moveFolder(oldPath, newPath); + } + + @Nullable + @Override + public DBSObjectFilter getSavedFilter(String name) { + return dataSourceRegistry.getSavedFilter(name); + } + + @NotNull + @Override + public List getSavedFilters() { + return dataSourceRegistry.getSavedFilters(); + } + + @Override + public void updateSavedFilter(@NotNull DBSObjectFilter filter) { + dataSourceRegistry.updateSavedFilter(filter); + } + + @Override + public void removeSavedFilter(@NotNull String filterName) { + dataSourceRegistry.removeSavedFilter(filterName); + } + + @Nullable + @Override + public DBWNetworkProfile getNetworkProfile(@Nullable String source, @NotNull String name) { + return dataSourceRegistry.getNetworkProfile(source, name); + } + + @NotNull + @Override + public List getNetworkProfiles() { + return dataSourceRegistry.getNetworkProfiles(); + } + + @Override + public void updateNetworkProfile(@NotNull DBWNetworkProfile profile) { + dataSourceRegistry.updateNetworkProfile(profile); + } + + @Override + public void removeNetworkProfile(@NotNull DBWNetworkProfile profile) { + dataSourceRegistry.removeNetworkProfile(profile); + } + + @Nullable + @Override + public DBAAuthProfile getAuthProfile(@NotNull String id) { + return dataSourceRegistry.getAuthProfile(id); + } + + @NotNull + @Override + public List getAllAuthProfiles() { + return dataSourceRegistry.getAllAuthProfiles(); + } + + @NotNull + @Override + public List getApplicableAuthProfiles(@Nullable DBPDriver driver) { + return dataSourceRegistry.getApplicableAuthProfiles(driver); + } + + @Override + public void updateAuthProfile(@NotNull DBAAuthProfile profile) { + dataSourceRegistry.updateAuthProfile(profile); + } + + @Override + public void setAuthProfiles(@NotNull Collection profiles) { + dataSourceRegistry.setAuthProfiles(profiles); + } + + @Override + public void removeAuthProfile(@NotNull DBAAuthProfile profile) { + dataSourceRegistry.removeAuthProfile(profile); + } + + @Override + public void flushConfig() { + dataSourceRegistry.flushConfig(); + } + + @Override + public void refreshConfig() { + dataSourceRegistry.refreshConfig(); + } + + @Override + public void refreshConfig(@Nullable Collection dataSourceIds) { + dataSourceRegistry.refreshConfig(dataSourceIds); + } + + @Nullable + @Override + public Throwable getLastError() { + return dataSourceRegistry.getLastError(); + } + + @Override + public boolean hasError() { + return dataSourceRegistry.hasError(); + } + + @Override + public void checkForErrors() throws DBException { + dataSourceRegistry.checkForErrors(); + } + + @Override + public void notifyDataSourceListeners(@NotNull DBPEvent event) { + dataSourceRegistry.notifyDataSourceListeners(event); + } + + @Nullable + @Override + public DBACredentialsProvider getAuthCredentialsProvider() { + return dataSourceRegistry.getAuthCredentialsProvider(); + } + + @Override + public void dispose() { + dataSourceRegistry.dispose(); + } + + @Override + public void setAuthCredentialsProvider(DBACredentialsProvider authCredentialsProvider) { + dataSourceRegistry.setAuthCredentialsProvider(authCredentialsProvider); + } + + @NotNull + @Override + public Set getTemporaryFolders() { + return dataSourceRegistry.getTemporaryFolders(); + } + + @NotNull + @Override + public DBPPreferenceStore getPreferenceStore() { + return dataSourceRegistry.getPreferenceStore(); + } + + @Override + public DataSourceParseResults loadDataSources( + @NotNull List storages, + @NotNull DataSourceConfigurationManager manager, + @Nullable Collection dataSourceIds, boolean refresh, + boolean purgeUntouched + ) { + return dataSourceRegistry.loadDataSources(storages, manager, dataSourceIds, refresh, purgeUntouched); + } + + @Override + public void saveDataSources() { + dataSourceRegistry.saveDataSources(); + } + + @Override + public DataSourceConfigurationManager getConfigurationManager() { + return dataSourceRegistry.getConfigurationManager(); + } + + @Override + public void saveConfigurationToManager( + @NotNull DBRProgressMonitor monitor, + @NotNull DataSourceConfigurationManager configurationManager, + @Nullable Predicate filter + ) { + dataSourceRegistry.saveConfigurationToManager(monitor, configurationManager, filter); + } + + @Override + public void persistSecrets(DBSSecretController secretController) throws DBException { + dataSourceRegistry.persistSecrets(secretController); + } + + @Override + public void resolveSecrets(DBSSecretController secretController) throws DBException { + dataSourceRegistry.resolveSecrets(secretController); + } + + /** + * Event listener proxy. + * For some cases (like creating data source) we should not send event because of accessibility of connection. + */ + private class WebDBPEventListenerProxy implements DBPEventListener { + @NotNull + private final DBPEventListener eventListener; + + public WebDBPEventListenerProxy(@NotNull DBPEventListener eventListener) { + this.eventListener = eventListener; + } + + @Override + public void handleDataSourceEvent(@NotNull DBPEvent event) { + if (event.getAction() == DBPEvent.Action.OBJECT_ADD && + event.getObject() instanceof DBPDataSourceContainer container && + !dataSourceFilter.filter(container) + ) { + // we cannot send event of creating data source connection because it is not accessible for user + return; + } + eventListener.handleDataSourceEvent(event); + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebHeadlessSessionProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebHeadlessSessionProjectImpl.java new file mode 100644 index 0000000000..852520ef2f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebHeadlessSessionProjectImpl.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import io.cloudbeaver.model.session.WebHeadlessSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.rm.RMUtils; + +public class WebHeadlessSessionProjectImpl extends WebProjectImpl { + public WebHeadlessSessionProjectImpl( + @NotNull WebHeadlessSession session, + @NotNull RMProject project + ) { + super( + session.getWorkspace(), + session.getUserContext().getRmController(), + session.getSessionContext(), + project, + session.getUserContext().getPreferenceStore(), + RMUtils.getProjectPath(project) + ); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebPage.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebPage.java index 1c21e6f1a4..240383848d 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebPage.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebPage.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java index b1b1775dd9..58ee233e45 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,29 +17,40 @@ package io.cloudbeaver; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMSessionContext; +import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.task.DBTTaskManager; +import org.jkiss.dbeaver.registry.DataSourceRegistry; import org.jkiss.dbeaver.registry.rm.DataSourceRegistryRM; import org.jkiss.dbeaver.runtime.DBWorkbench; +import java.nio.file.Path; + public abstract class WebProjectImpl extends BaseWebProjectImpl { private static final Log log = Log.getLog(WebProjectImpl.class); + @NotNull + protected final DBPPreferenceStore preferenceStore; public WebProjectImpl( @NotNull DBPWorkspace workspace, @NotNull RMController resourceController, @NotNull SMSessionContext sessionContext, @NotNull RMProject project, - @NotNull DataSourceFilter dataSourceFilter + @NotNull DBPPreferenceStore preferenceStore, + @NotNull Path path ) { - super(workspace, resourceController, sessionContext, project, dataSourceFilter); + super(workspace, resourceController, sessionContext, project, path); + this.preferenceStore = preferenceStore; } + @Nullable @Override public Object getProjectProperty(String propName) { try { @@ -51,7 +62,7 @@ public Object getProjectProperty(String propName) { } @Override - public void setProjectProperty(String propName, Object propValue) { + public void setProjectProperty(@NotNull String propName, @Nullable Object propValue) { try { getResourceController().setProjectProperty(getId(), propName, propValue); } catch (DBException e) { @@ -64,13 +75,21 @@ public boolean isUseSecretStorage() { return DBWorkbench.isDistributed(); } + @NotNull + @Override + public DBTTaskManager getTaskManager() { + throw new IllegalStateException("Task manager not supported"); + } + @NotNull @Override protected DBPDataSourceRegistry createDataSourceRegistry() { - return new WebDataSourceRegistryProxy( - new DataSourceRegistryRM(this, getResourceController()), - dataSourceFilter - ); + return createRMRegistry(); + } + + @NotNull + protected DataSourceRegistry createRMRegistry() { + return new DataSourceRegistryRM<>(this, getResourceController(), preferenceStore); } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionGlobalProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionGlobalProjectImpl.java new file mode 100644 index 0000000000..f8185624a7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionGlobalProjectImpl.java @@ -0,0 +1,122 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.DBPEvent; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.security.SMObjectType; +import org.jkiss.dbeaver.model.security.user.SMObjectPermissions; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Global project. + * Connections there can be not accessible. + */ +public class WebSessionGlobalProjectImpl extends WebSessionProjectImpl { + private static final Log log = Log.getLog(WebSessionGlobalProjectImpl.class); + + private final Set accessibleConnectionIds = new CopyOnWriteArraySet<>(); + + public WebSessionGlobalProjectImpl(@NotNull WebSession webSession, @NotNull RMProject project) { + super(webSession, project); + } + + /** + * Creates data source registry that can filter data sources that are not accessible for user. + */ + @NotNull + @Override + protected DBPDataSourceRegistry createDataSourceRegistry() { + return new WebGlobalProjectRegistryProxy( + webSession, + createRegistryWithCredentialsProvider(), + this::isDataSourceAccessible + ); + } + + /** + * Update info about accessible connections from a database. + */ + public void refreshAccessibleConnectionIds() { + this.accessibleConnectionIds.clear(); + try { + for (SMObjectPermissions smObjectPermissions : webSession.getSecurityController() + .getAllAvailableObjectsPermissions(SMObjectType.datasource)) { + String objectId = smObjectPermissions.getObjectId(); + this.accessibleConnectionIds.add(objectId); + } + } catch (DBException e) { + webSession.addSessionError(e); + log.error("Error reading connection grants", e); + } + } + + /** + * Checks if connection is accessible for current user. + */ + public boolean isDataSourceAccessible(@NotNull DBPDataSourceContainer dataSource) { + return dataSource.isExternallyProvided() || + dataSource.isTemporary() || + webSession.hasPermission(DBWConstants.PERMISSION_ADMIN) || + accessibleConnectionIds.contains(dataSource.getId()); + } + + /** + * Adds a connection if it became accessible. + * The method is processed when connection permissions were updated. + */ + public synchronized void addAccessibleConnectionToCache(@NotNull String dsId) { + if (!getRMProject().isGlobal()) { + return; + } + this.accessibleConnectionIds.add(dsId); + var registry = getDataSourceRegistry(); + var dataSource = registry.getDataSource(dsId); + if (dataSource != null) { + addConnection(dataSource); + // reflect changes is navigator model + registry.notifyDataSourceListeners(new DBPEvent(DBPEvent.Action.OBJECT_ADD, dataSource, true)); + } + } + + /** + * Removes a connection if it became not accessible. + * The method is processed when connection permissions were updated. + */ + public synchronized void removeAccessibleConnectionFromCache(@NotNull String dsId) { + if (!getRMProject().isGlobal()) { + return; + } + var registry = getDataSourceRegistry(); + var dataSource = registry.getDataSource(dsId); + if (dataSource != null) { + this.accessibleConnectionIds.remove(dsId); + removeConnection(dataSource); + // reflect changes is navigator model + registry.notifyDataSourceListeners(new DBPEvent(DBPEvent.Action.OBJECT_REMOVE, dataSource)); + dataSource.dispose(); + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java index 938b8e31f5..7e5ec7a754 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,18 +16,64 @@ */ package io.cloudbeaver; +import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.utils.WebDataSourceUtils; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistryCache; import org.jkiss.dbeaver.model.navigator.DBNModel; import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.rm.RMUtils; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; +import org.jkiss.dbeaver.registry.DataSourceDescriptor; +import org.jkiss.dbeaver.registry.DataSourceRegistry; +import org.jkiss.dbeaver.runtime.jobs.DisconnectJob; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class WebSessionProjectImpl extends WebProjectImpl { + private static final Log log = Log.getLog(WebSessionProjectImpl.class); + protected final WebSession webSession; + private final Map connections = new HashMap<>(); + private boolean registryIsLoaded = false; - private final WebSession webSession; + public WebSessionProjectImpl( + @NotNull WebSession webSession, + @NotNull RMProject project + ) { + super( + webSession.getWorkspace(), + webSession.getRmController(), + webSession.getSessionContext(), + project, + webSession.getUserPreferenceStore(), + RMUtils.getProjectPath(project) + ); + this.webSession = webSession; + } - public WebSessionProjectImpl(@NotNull WebSession webSession, @NotNull RMProject project, @NotNull DataSourceFilter dataSourceFilter) { - super(webSession.getWorkspace(), webSession.getRmController(), webSession.getSessionContext(), project, dataSourceFilter); + public WebSessionProjectImpl( + @NotNull WebSession webSession, + @NotNull RMProject project, + @NotNull Path path + ) { + super( + webSession.getWorkspace(), + webSession.getRmController(), + webSession.getSessionContext(), + project, + webSession.getUserPreferenceStore(), + path + ); this.webSession = webSession; } @@ -36,4 +82,159 @@ public WebSessionProjectImpl(@NotNull WebSession webSession, @NotNull RMProject public DBNModel getNavigatorModel() { return webSession.getNavigatorModel(); } + + @NotNull + @Override + protected DBPDataSourceRegistry createDataSourceRegistry() { + return createRegistryWithCredentialsProvider(); + } + + @NotNull + protected DataSourceRegistry createRegistryWithCredentialsProvider() { + DataSourceRegistry dataSourceRegistry = createRMRegistry(); + dataSourceRegistry.setAuthCredentialsProvider(webSession); + dataSourceRegistry.addDataSourceListener(webSession.getDataSourceConnectListener()); + return dataSourceRegistry; + } + + private synchronized void addDataSourcesToCache() { + if (registryIsLoaded) { + return; + } + getDataSourceRegistry().getDataSources().forEach(this::addConnection); + Throwable lastError = getDataSourceRegistry().getLastError(); + if (lastError != null) { + webSession.addSessionError(lastError); + log.error("Error refreshing connections from project '" + getId() + "'", lastError); + } + registryIsLoaded = true; + } + + @Override + public void dispose() { + super.dispose(); + Map conCopy; + synchronized (this.connections) { + conCopy = new HashMap<>(this.connections); + this.connections.clear(); + } + + for (WebConnectionInfo connectionInfo : conCopy.values()) { + if (connectionInfo.isConnected()) { + new DisconnectJob(connectionInfo.getDataSourceContainer()).schedule(); + } + } + } + + + /** + * Returns web connection info from cache (if exists). + */ + @Nullable + public WebConnectionInfo findWebConnectionInfo(@NotNull String connectionId) { + synchronized (connections) { + return connections.get(connectionId); + } + } + + /** + * Returns web connection info from cache, adds it to cache if not present. + * Throws exception if connection is not found. + */ + @NotNull + public WebConnectionInfo getWebConnectionInfo(@NotNull String connectionId) throws DBWebException { + WebConnectionInfo connectionInfo = findWebConnectionInfo(connectionId); + if (connectionInfo != null) { + return connectionInfo; + } + DBPDataSourceContainer dataSource = getDataSourceRegistry().getDataSource(connectionId); + if (dataSource != null) { + return addConnection(dataSource); + } + throw new DBWebException("Connection '%s' not found".formatted(connectionId)); + } + + /** + * Adds connection to project cache. + */ + @NotNull + public synchronized WebConnectionInfo addConnection(@NotNull DBPDataSourceContainer dataSourceContainer) { + WebConnectionInfo connection = new WebConnectionInfo(webSession, dataSourceContainer); + synchronized (connections) { + connections.put(dataSourceContainer.getId(), connection); + } + return connection; + } + + /** + * Removes connection from project cache. + */ + public void removeConnection(@NotNull DBPDataSourceContainer dataSourceContainer) { + WebConnectionInfo webConnectionInfo = connections.get(dataSourceContainer.getId()); + if (webConnectionInfo != null) { + webConnectionInfo.clearCache(); + synchronized (connections) { + connections.remove(dataSourceContainer.getId()); + } + } + } + + /** + * Loads connection from registry if they are not loaded. + * + * @return connections from cache. + */ + public List getConnections() { + if (!registryIsLoaded) { + addDataSourcesToCache(); + registryIsLoaded = true; + } + synchronized (connections) { + return new ArrayList<>(connections.values()); + } + } + + /** + * updates data sources based on event in web session + * + * @param event data source updated event + */ + public synchronized boolean updateProjectDataSources(@NotNull WSDataSourceEvent event) { + var sendDataSourceUpdatedEvent = false; + DBPDataSourceRegistry registry = getDataSourceRegistry(); + if (WSDataSourceEvent.CREATED.equals(event.getId()) || WSDataSourceEvent.UPDATED.equals(event.getId())) { + registry.refreshConfig(event.getDataSourceIds()); + } + for (String dsId : event.getDataSourceIds()) { + DataSourceDescriptor ds = (DataSourceDescriptor) registry.getDataSource(dsId); + if (ds == null) { + continue; + } + switch (event.getId()) { + case WSDataSourceEvent.CREATED -> { + addConnection(ds); + sendDataSourceUpdatedEvent = true; + } + case WSDataSourceEvent.UPDATED -> { + if (event.getProperty() == WSDataSourceProperty.CONFIGURATION) { + WebDataSourceUtils.disconnectDataSource(webSession, ds); + } + if (event.getProperty() != WSDataSourceProperty.INTERNAL) { + sendDataSourceUpdatedEvent = true; + } + } + case WSDataSourceEvent.DELETED -> { + WebDataSourceUtils.disconnectDataSource(webSession, ds); + if (registry instanceof DBPDataSourceRegistryCache dsrc) { + dsrc.removeDataSourceFromList(ds); + } + removeConnection(ds); + sendDataSourceUpdatedEvent = true; + } + default -> { + } + } + } + return sendDataSourceUpdatedEvent; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java index 2bccdc5f71..24769da64f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/CBAuthConstants.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -25,6 +25,7 @@ public interface CBAuthConstants { String CB_AUTH_ID_REQUEST_PARAM = "authId"; String CB_AUTO_LOGIN_REQUEST_PARAM = "autoLogin"; String CB_REDIRECT_URL_REQUEST_PARAM = "redirectUrl"; + String CB_FORCE_SESSIONS_LOGOUT = "forceSessionsLogout"; // Default max idle time (10 minutes) long MAX_SESSION_IDLE_TIME = 10 * 60 * 1000; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/NoAuthCredentialsProvider.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/NoAuthCredentialsProvider.java index d95301f7ee..da6181c253 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/NoAuthCredentialsProvider.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/NoAuthCredentialsProvider.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderAssigner.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderAssigner.java index 9c3f432b39..1cda0da016 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderAssigner.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderAssigner.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -17,6 +17,7 @@ package io.cloudbeaver.auth; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; @@ -36,6 +37,7 @@ SMAutoAssign detectAutoAssignments( @NotNull Map authParameters ) throws DBException; + @Nullable String getExternalTeamIdMetadataFieldName(); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderExternal.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderExternal.java index d2f5f5f350..d0d79f415f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderExternal.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderExternal.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -69,4 +69,9 @@ DBPObject getUserDetails( @NotNull WebUser user, boolean selfIdentity) throws DBException; + /** + * Make some post authentication actions + */ + default void postAuthentication() {} + } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderFederated.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderFederated.java index c76504084b..87bd511a23 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderFederated.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderFederated.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -19,6 +19,7 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; import java.util.Map; @@ -26,23 +27,53 @@ * Federated auth provider. * Provides links to external auth resource */ -public interface SMAuthProviderFederated { +public interface SMAuthProviderFederated extends SMSignOutLinkProvider { - /** - * Returns new identifying credentials which can be used to find/create user in database - */ @NotNull - String getSignInLink(String id, @NotNull Map providerConfig) throws DBException; + String getSignInLink(@NotNull String id, @NotNull String origin) throws DBException; + @Override + default String getUserSignOutLink( + @NotNull SMAuthProviderCustomConfiguration providerConfig, + @NotNull Map userCredentials, + @NotNull String origin + ) throws DBException { + return getCommonSignOutLink(providerConfig.getId(), providerConfig.getParameters(), origin); + } - @NotNull - String getSignOutLink(String id, @NotNull Map providerConfig) throws DBException; + @Nullable + default String getMetadataLink( + @NotNull String providerId, + @NotNull Map providerConfig, + @NotNull String origin + ) throws DBException { + return null; + } @Nullable - String getMetadataLink(String id, @NotNull Map providerConfig) throws DBException; + default String getAcsLink( + @NotNull String providerId, + @NotNull Map providerConfig, + @NotNull String origin + ) throws DBException { + return null; + } + + @Nullable + default String getEntityIdLink( + @NotNull String providerId, + @NotNull Map providerConfig, + @NotNull String origin + ) throws DBException { + return null; + } @Nullable - default String getRedirectLink(String id, @NotNull Map providerConfig) throws DBException { + default String getRedirectLink( + @NotNull String providerId, + @NotNull Map providerConfig, + @NotNull String origin + ) throws DBException { return null; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAutoAssign.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAutoAssign.java index ce3398fbef..a47a167bd9 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAutoAssign.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAutoAssign.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -17,11 +17,14 @@ package io.cloudbeaver.auth; +import org.jkiss.code.Nullable; + import java.util.ArrayList; import java.util.List; public class SMAutoAssign { private String authRole; + private String authRoleAssignReason; private List externalTeamIds = new ArrayList<>(); public SMAutoAssign() { @@ -47,4 +50,13 @@ public List getExternalTeamIds() { public void addExternalTeamId(String externalRoleId) { this.externalTeamIds.add(externalRoleId); } + + @Nullable + public String getAuthRoleAssignReason() { + return authRoleAssignReason; + } + + public void setAuthRoleAssignReason(@Nullable String authRoleChangedReason) { + this.authRoleAssignReason = authRoleChangedReason; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMBruteForceProtected.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMBruteForceProtected.java new file mode 100644 index 0000000000..57126cc27d --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMBruteForceProtected.java @@ -0,0 +1,25 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.auth; + +import org.jkiss.code.NotNull; + +import java.util.Map; + +public interface SMBruteForceProtected { + Object getInputUsername(@NotNull Map cred); +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMSignOutLinkProvider.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMSignOutLinkProvider.java new file mode 100644 index 0000000000..46d9d7b99a --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMSignOutLinkProvider.java @@ -0,0 +1,42 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.auth; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; + +import java.util.Map; + +public interface SMSignOutLinkProvider { + + /** + * @return a common link for logout, not related with the user context + */ + @NotNull + String getCommonSignOutLink( + @NotNull String providerId, + @NotNull Map providerConfig, + @NotNull String origin + ) throws DBException; + + String getUserSignOutLink( + @NotNull SMAuthProviderCustomConfiguration providerConfig, + @NotNull Map userCredentials, + @NotNull String origin + ) throws DBException; +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMTokenCredentialProvider.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMTokenCredentialProvider.java index 1716be7503..62f17cf116 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMTokenCredentialProvider.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMTokenCredentialProvider.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java index 5191eae5e1..654f9831c8 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -24,7 +24,8 @@ /** * Abstract external auth provider */ -public abstract class AbstractExternalAuthProvider implements SMAuthProviderExternal { +public abstract class AbstractExternalAuthProvider + implements SMAuthProviderExternal { public static final String META_AUTH_PROVIDER = "$provider"; public static final String META_AUTH_SPACE_ID = "$space"; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/fa/AbstractSessionExternal.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/fa/AbstractSessionExternal.java new file mode 100644 index 0000000000..f0ac26016a --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/fa/AbstractSessionExternal.java @@ -0,0 +1,79 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.auth.provider.fa; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.auth.*; + +import java.time.LocalDateTime; +import java.util.Map; + +public abstract class AbstractSessionExternal implements SMSessionExternal { + + @NotNull + protected final Map authParameters; + @NotNull + protected final SMSession parentSession; + @NotNull + protected final SMAuthSpace space; + + protected AbstractSessionExternal( + @NotNull SMSession parentSession, + @NotNull SMAuthSpace space, + @NotNull Map authParameters + ) { + this.parentSession = parentSession; + this.space = space; + this.authParameters = authParameters; + } + + @NotNull + @Override + public SMAuthSpace getSessionSpace() { + return space; + } + + @NotNull + @Override + public SMSessionContext getSessionContext() { + return this.parentSession.getSessionContext(); + } + + @Nullable + @Override + public SMSessionPrincipal getSessionPrincipal() { + return parentSession.getSessionPrincipal(); + } + + @NotNull + @Override + public LocalDateTime getSessionStart() { + return parentSession.getSessionStart(); + } + + @Override + public void close() { + // do nothing + } + + @Override + public Map getAuthParameters() { + return authParameters; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/local/LocalAuthProviderConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/local/LocalAuthProviderConstants.java index fe7ef5bf78..3e260bb150 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/local/LocalAuthProviderConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/local/LocalAuthProviderConstants.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -21,5 +21,6 @@ public class LocalAuthProviderConstants { public static final String PROVIDER_ID = "local"; public static final String CRED_USER = "user"; + public static final String CRED_DISPLAY_NAME = "displayName"; public static final String CRED_PASSWORD = "password"; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provisioning/SMProvisioner.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provisioning/SMProvisioner.java index 730460c206..291b73ea1a 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provisioning/SMProvisioner.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provisioning/SMProvisioner.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -31,4 +31,8 @@ List listExternalUsers( @NotNull SMAuthProviderCustomConfiguration customConfiguration, @NotNull SMProvisioningFilter filter ) throws DBException; + + default boolean isAuthRoleProvided(SMAuthProviderCustomConfiguration configuration) { + return false; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/BaseDatasourceAccessCheckHandler.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/BaseDatasourceAccessCheckHandler.java new file mode 100644 index 0000000000..ff9cb43e9e --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/BaseDatasourceAccessCheckHandler.java @@ -0,0 +1,30 @@ +package io.cloudbeaver.model; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.DBPDataSourceHandler; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; + +public abstract class BaseDatasourceAccessCheckHandler implements DBPDataSourceHandler { + @Override + public void beforeConnect( + DBRProgressMonitor monitor, + @NotNull DBPDataSourceContainer dataSourceContainer + ) throws DBException { + if (isDriverDisabled(dataSourceContainer.getDriver())) { + throw new DBException("Driver disabled"); + } + } + + @Override + public void beforeDisconnect( + DBRProgressMonitor monitor, + @NotNull DBPDataSourceContainer dataSourceContainer + ) throws DBException { + + } + + protected abstract boolean isDriverDisabled(DBPDriver driver); +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/CustomCancelableJob.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/CustomCancelableJob.java new file mode 100644 index 0000000000..6ccc7159fe --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/CustomCancelableJob.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model; + +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; + +public interface CustomCancelableJob { + void cancelJob(@NotNull WebSession webSession, @NotNull WebAsyncTaskInfo taskInfo); +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebAsyncTaskInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebAsyncTaskInfo.java index a3b13ee416..6930bb32e1 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebAsyncTaskInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebAsyncTaskInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,44 +16,42 @@ */ package io.cloudbeaver.model; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.runtime.AbstractJob; /** - * Web connection info + * Web async task info */ public class WebAsyncTaskInfo { - private String id; - private String name; - private boolean running; + @NotNull + private final String id; + @NotNull + private final String name; + private boolean running = false; private Object result; private Object extendedResult; private String status; private Throwable jobError; private AbstractJob job; + private boolean cancelled = false; - public WebAsyncTaskInfo(String id, String name) { + public WebAsyncTaskInfo(@NotNull String id, @NotNull String name) { this.id = id; this.name = name; } + @NotNull public String getId() { return id; } - public void setId(String id) { - this.id = id; - } - + @NotNull public String getName() { return name; } - public void setName(String name) { - this.name = name; - } - public boolean isRunning() { return running; } @@ -110,5 +108,4 @@ public AbstractJob getJob() { public void setJob(AbstractJob job) { this.job = job; } - } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebBasicObjectInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebBasicObjectInfo.java index 1c907fb7b3..05e445031d 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebBasicObjectInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebBasicObjectInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionConfig.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionConfig.java new file mode 100644 index 0000000000..d2787d958d --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionConfig.java @@ -0,0 +1,269 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model; + +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.connection.DBPDriverConfigurationType; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.utils.CommonUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Web connection config + */ +public class WebConnectionConfig { + + private String connectionId; + private String driverId; + + private boolean readOnly; + + private String host; + private String port; + private String serverName; + private String databaseName; + private String url; + + private int keepAliveInterval; + + private String name; + private String description; + private String folder; + private Map properties; + private String userName; + private String userPassword; + + private String authModelId; + private Map credentials; + private boolean saveCredentials; + private boolean sharedCredentials; + private Map mainPropertyValues; + private Map expertSettingsValues; + private Map providerProperties; + private Map externalParameters; + private List networkHandlersConfig; + private DBPDriverConfigurationType configurationType; + private String selectedSecretId; + private boolean defaultAutoCommit; + private String defaultCatalogName; + private String defaultSchemaName; + + public WebConnectionConfig() { + } + + public WebConnectionConfig(Map params) { + if (!CommonUtils.isEmpty(params)) { + connectionId = JSONUtils.getString(params, "connectionId"); + driverId = JSONUtils.getString(params, "driverId"); + + host = JSONUtils.getString(params, "host"); + port = JSONUtils.getString(params, "port"); + serverName = JSONUtils.getString(params, "serverName"); + databaseName = JSONUtils.getString(params, "databaseName"); + url = JSONUtils.getString(params, "url"); + + name = JSONUtils.getString(params, "name"); + description = JSONUtils.getString(params, "description"); + folder = JSONUtils.getString(params, "folder"); + + properties = JSONUtils.getObjectOrNull(params, "properties"); + userName = JSONUtils.getString(params, "userName"); + userPassword = JSONUtils.getString(params, "userPassword"); + selectedSecretId = JSONUtils.getString(params, "selectedSecretId"); + + authModelId = JSONUtils.getString(params, "authModelId"); + credentials = JSONUtils.getObjectOrNull(params, "credentials"); + saveCredentials = JSONUtils.getBoolean(params, "saveCredentials"); + sharedCredentials = JSONUtils.getBoolean(params, "sharedCredentials"); + + mainPropertyValues = JSONUtils.getObjectOrNull(params, "mainPropertyValues"); + providerProperties = JSONUtils.getObjectOrNull(params, "providerProperties"); + + expertSettingsValues = JSONUtils.getObjectOrNull(params, "expertSettingsValues"); + keepAliveInterval = JSONUtils.getInteger( + expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_KEEP_ALIVE_INTERVAL, -1); + readOnly = JSONUtils.getBoolean( + expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_READ_ONLY); + defaultAutoCommit = JSONUtils.getBoolean( + expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_AUTO_COMMIT, true); + defaultCatalogName = JSONUtils.getString( + expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_DEFAULT_CATALOG); + defaultSchemaName = JSONUtils.getString( + expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_DEFAULT_SCHEMA); + + String configType = JSONUtils.getString(params, "configurationType"); + configurationType = configType == null ? null : DBPDriverConfigurationType.valueOf(configType); + externalParameters = JSONUtils.getObjectOrNull(params, "externalParameters"); + + networkHandlersConfig = new ArrayList<>(); + for (Map nhc : JSONUtils.getObjectList(params, "networkHandlersConfig")) { + networkHandlersConfig.add(new WebNetworkHandlerConfigInput(nhc)); + } + } + } + + @Property + public String getConnectionId() { + return connectionId; + } + + @Property + public String getDriverId() { + return driverId; + } + + @Property + public boolean isReadOnly() { + return readOnly; + } + + @Property + public String getName() { + return name; + } + + @Property + public String getDescription() { + return description; + } + + @Property + public String getFolder() { + return folder; + } + + @Property + public String getHost() { + return host; + } + + @Property + public String getPort() { + return port; + } + + @Property + public String getServerName() { + return serverName; + } + + @Property + public String getDatabaseName() { + return databaseName; + } + + @Property + public String getUrl() { + return url; + } + + @Property + public Map getProperties() { + return properties; + } + + @Property + public String getUserName() { + return userName; + } + + @Property + public String getUserPassword() { + return userPassword; + } + + @Property + public String getAuthModelId() { + return authModelId; + } + + @Property + public DBPDriverConfigurationType getConfigurationType() { + return configurationType; + } + + @Property + public Map getCredentials() { + return credentials; + } + + public List getNetworkHandlersConfig() { + return networkHandlersConfig; + } + + @Property + public boolean isSaveCredentials() { + return saveCredentials; + } + + @Property + public boolean isSharedCredentials() { + return sharedCredentials; + } + + public void setSaveCredentials(boolean saveCredentials) { + this.saveCredentials = saveCredentials; + } + + @Property + public Map getMainPropertyValues() { + return mainPropertyValues; + } + + @Property + public Map getExpertSettingsValues() { + return expertSettingsValues; + } + + @Property + public Map getProviderProperties() { + return providerProperties; + } + + @Property + public Integer getKeepAliveInterval() { + return keepAliveInterval; + } + + @Property + public Boolean isDefaultAutoCommit() { + return defaultAutoCommit; + } + + @Nullable + public String getSelectedSecretId() { + return selectedSecretId; + } + + @Property + public String getDefaultCatalogName() { + return defaultCatalogName; + } + + @Property + public String getDefaultSchemaName() { + return defaultSchemaName; + } + + public Map getExternalParameters() { + return externalParameters; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionFolderInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionFolderInfo.java index 698dc8c5ec..a6452adb6f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionFolderInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionFolderInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java index 91dbbce1bb..a49b1f16b5 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -22,14 +22,14 @@ import io.cloudbeaver.service.security.SMUtils; import io.cloudbeaver.service.sql.WebDataFormat; import io.cloudbeaver.utils.CBModelConstants; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.utils.ServletAppUtils; import io.cloudbeaver.utils.WebCommonUtils; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.DBConstants; -import org.jkiss.dbeaver.model.DBPDataSource; -import org.jkiss.dbeaver.model.DBPDataSourceContainer; -import org.jkiss.dbeaver.model.DBPDataSourceFolder; +import org.jkiss.dbeaver.model.*; +import org.jkiss.dbeaver.model.admin.sessions.DBAServerSessionManager; import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.connection.DBPAuthModelDescriptor; import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; @@ -40,8 +40,11 @@ import org.jkiss.dbeaver.model.navigator.DBNDataSource; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; import org.jkiss.dbeaver.model.preferences.DBPPropertySource; +import org.jkiss.dbeaver.model.rm.RMConstants; import org.jkiss.dbeaver.model.rm.RMProjectPermission; import org.jkiss.dbeaver.model.runtime.DBRRunnableParametrized; +import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor; +import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.CommonUtils; @@ -56,6 +59,17 @@ public class WebConnectionInfo { private static final Log log = Log.getLog(WebConnectionInfo.class); public static final String SECURED_VALUE = "********"; + + private static final String FEATURE_HAS_TOOLS = "hasTools"; + private static final String FEATURE_CONNECTED = "connected"; + private static final String FEATURE_VIRTUAL = "virtual"; + private static final String FEATURE_TEMPORARY = "temporary"; + private static final String FEATURE_READ_ONLY = "readOnly"; + private static final String FEATURE_PROVIDED = "provided"; + private static final String FEATURE_MANAGEABLE = "manageable"; + + private static final String TOOL_SESSION_MANAGER = "sessionManager"; + private final WebSession session; private final DBPDataSourceContainer dataSourceContainer; private WebServerError connectError; @@ -63,6 +77,8 @@ public class WebConnectionInfo { private String connectTime; private String serverVersion; private String clientVersion; + @Nullable + private Boolean credentialsSavedInSession; private transient Map savedAuthProperties; private transient List savedNetworkCredentials; @@ -155,11 +171,6 @@ public boolean isConnected() { return dataSourceContainer.isConnected(); } - @Property - public boolean isTemplate() { - return dataSourceContainer.isTemplate(); - } - @Property public boolean isProvided() { return dataSourceContainer.isProvided(); @@ -182,7 +193,8 @@ public boolean isSaveCredentials() { @Property public boolean isCredentialsSaved() throws DBException { - return dataSourceContainer.isCredentialsSaved(); + // isCredentialsSaved can be true if credentials were saved during connection init for global project + return dataSourceContainer.isCredentialsSaved() && !(credentialsSavedInSession != null && credentialsSavedInSession); } @Property @@ -243,22 +255,25 @@ public String[] getFeatures() { List features = new ArrayList<>(); if (dataSourceContainer.isConnected()) { - features.add("connected"); + features.add(FEATURE_CONNECTED); + if (!getTools().isEmpty()) { + features.add(FEATURE_HAS_TOOLS); + } } if (dataSourceContainer.isHidden()) { - features.add("virtual"); + features.add(FEATURE_VIRTUAL); } if (dataSourceContainer.isTemporary()) { - features.add("temporary"); + features.add(FEATURE_TEMPORARY); } if (dataSourceContainer.isConnectionReadOnly()) { - features.add("readOnly"); + features.add(FEATURE_READ_ONLY); } if (dataSourceContainer.isProvided()) { - features.add("provided"); + features.add(FEATURE_PROVIDED); } if (dataSourceContainer.isManageable()) { - features.add("manageable"); + features.add(FEATURE_MANAGEABLE); } return features.toArray(new String[0]); @@ -342,8 +357,16 @@ public WebPropertyInfo[] getAuthProperties() { @Property public List getNetworkHandlersConfig() { - return dataSourceContainer.getConnectionConfiguration().getHandlers().stream() - .map(WebNetworkHandlerConfig::new).collect(Collectors.toList()); + var registry = NetworkHandlerRegistry.getInstance(); + return dataSourceContainer.getConnectionConfiguration() + .getHandlers() + .stream() + .filter(handlerConf -> { + NetworkHandlerDescriptor descriptor = registry.getDescriptor(handlerConf.getId()); + return descriptor != null && !descriptor.isDesktopHandler(); + }) + .map(WebNetworkHandlerConfig::new) + .collect(Collectors.toList()); } @Property @@ -384,6 +407,27 @@ public void fireCloseListeners() { } } + @Property + public Map getMainPropertyValues() { + Map mainProperties = new LinkedHashMap<>(); + mainProperties.put(DBConstants.PROP_HOST, getHost()); + mainProperties.put(DBConstants.PROP_PORT, getPort()); + mainProperties.put(DBConstants.PROP_DATABASE, getDatabaseName()); + mainProperties.put(DBConstants.PROP_SERVER, getServerName()); + return mainProperties; + } + + @Property + public Map getExpertSettingsValues() { + Map expertSettings = new LinkedHashMap<>(); + expertSettings.put(WebExpertSettingsProperties.PROP_AUTO_COMMIT, isAutocommit()); + expertSettings.put(WebExpertSettingsProperties.PROP_KEEP_ALIVE_INTERVAL, getKeepAliveInterval()); + expertSettings.put(WebExpertSettingsProperties.PROP_READ_ONLY, isReadOnly()); + expertSettings.put(WebExpertSettingsProperties.PROP_DEFAULT_CATALOG, getDefaultCatalogName()); + expertSettings.put(WebExpertSettingsProperties.PROP_DEFAULT_SCHEMA, getDefaultSchemaName()); + return expertSettings; + } + @Property public Map getProviderProperties() { return dataSourceContainer.getConnectionConfiguration().getProviderProperties(); @@ -421,17 +465,18 @@ public String getRequiredAuth() { private boolean hasProjectPermission(RMProjectPermission projectPermission) { DBPProject project = dataSourceContainer.getProject(); - if (!(project instanceof WebProjectImpl)) { + if (!(project instanceof WebProjectImpl webProject)) { return false; } - return SMUtils.hasProjectPermission(session, ((WebProjectImpl) project).getRmProject(), projectPermission); + return SMUtils.hasProjectPermission(session, webProject.getRMProject(), projectPermission); } private boolean canViewReadOnlyConnections() { if (isCanEdit()) { return true; } - BaseWebAppConfiguration appConfig = (BaseWebAppConfiguration) WebAppUtils.getWebApplication().getAppConfiguration(); + BaseWebAppConfiguration appConfig = (BaseWebAppConfiguration) ServletAppUtils.getServletApplication() + .getAppConfiguration(); return appConfig.isShowReadOnlyConnectionInfo(); } @@ -443,4 +488,58 @@ public void addCloseListener(DBRRunnableParametrized listener closeListeners.add(listener); } + @Property + public int getKeepAliveInterval() { + return dataSourceContainer.getConnectionConfiguration().getKeepAliveInterval(); + } + + @Property + public boolean isAutocommit() { + Boolean isAutoCommit = dataSourceContainer.getConnectionConfiguration().getBootstrap().getDefaultAutoCommit(); + if (isAutoCommit == null) { + return true; + } + return isAutoCommit; + } + + @Property + public String getDefaultCatalogName() { + DBPConnectionConfiguration connectionConfiguration = dataSourceContainer.getConnectionConfiguration(); + return connectionConfiguration.getBootstrap().getDefaultCatalogName(); + } + + @Property + public String getDefaultSchemaName() { + DBPConnectionConfiguration connectionConfiguration = dataSourceContainer.getConnectionConfiguration(); + return connectionConfiguration.getBootstrap().getDefaultSchemaName(); + } + + @Property + public List getSharedSecrets() throws DBException { + return dataSourceContainer.listSharedCredentials() + .stream() + .map(WebSecretInfo::new) + .collect(Collectors.toList()); + } + + @NotNull + @Property + public List getTools() { + if (!session.hasPermission(RMConstants.PERMISSION_DATABASE_DEVELOPER)) { + return List.of(); + } + List tools = new ArrayList<>(); + // checks inside that datasource is not null in container, and it is adaptable to session manager class + if (DBUtils.getAdapter(DBAServerSessionManager.class, dataSourceContainer) != null) { + tools.add(TOOL_SESSION_MANAGER); + } + return tools; + } + + /** + * Updates param that checks whether credentials were saved only in session. + */ + public void setCredentialsSavedInSession(@Nullable Boolean credentialsSavedInSession) { + this.credentialsSavedInSession = credentialsSavedInSession; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionOriginInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionOriginInfo.java index 2b88b5a8d4..86f078cbb1 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionOriginInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionOriginInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebExpertSettingsProperties.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebExpertSettingsProperties.java new file mode 100644 index 0000000000..800d0f349a --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebExpertSettingsProperties.java @@ -0,0 +1,87 @@ +package io.cloudbeaver.model; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.DBPObject; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.model.connection.DBPDriverConstants; +import org.jkiss.dbeaver.model.meta.IPropertyValueValidator; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.utils.CommonUtils; + +/** + * Web expert settings properties. Class for returning filtered expert settings properties in web interface. + */ +public class WebExpertSettingsProperties implements DBPObject { + public static final String PROP_READ_ONLY = "readOnly"; + public static final String PROP_AUTO_COMMIT = "autocommit"; + public static final String PROP_KEEP_ALIVE_INTERVAL = "keepAliveInterval"; + public static final String PROP_DEFAULT_CATALOG = "defaultCatalogName"; + public static final String PROP_DEFAULT_SCHEMA = "defaultSchemaName"; + + private final DBPDriver driver; + + public WebExpertSettingsProperties(@NotNull DBPDriver driver) { + this.driver = driver; + } + + @Property(order = 1, id = PROP_KEEP_ALIVE_INTERVAL, visibleIf = KeepAliveIntervalFieldValidator.class) + public int getKeepAliveInterval() { + return 0; + } + + @Property(order = 2, id = PROP_AUTO_COMMIT, visibleIf = AutoCommitFieldValidator.class) + public boolean isAutoCommit() { + return true; + } + + @Property(order = 3, id = PROP_READ_ONLY, visibleIf = ReadOnlyFieldValidator.class) + public boolean isReadOnly() { + return false; + } + + @Property(order = 4, id = PROP_DEFAULT_CATALOG, visibleIf = DefaultCatalogFieldVisibleValidator.class) + public String getDefaultCatalog() { + return null; + } + + @Property(order = 5, id = PROP_DEFAULT_SCHEMA, visibleIf = DefaultSchemaFieldVisibleValidator.class) + public String getDefaultSchema() { + return null; + } + + + public static class KeepAliveIntervalFieldValidator implements IPropertyValueValidator { + @Override + public boolean isValidValue(WebExpertSettingsProperties object, Object value) throws IllegalArgumentException { + return CommonUtils.toBoolean(!object.driver.isEmbedded(), true); + } + } + + public static class AutoCommitFieldValidator implements IPropertyValueValidator { + @Override + public boolean isValidValue(WebExpertSettingsProperties object, Object value) throws IllegalArgumentException { + return CommonUtils.toBoolean(object.driver.getDriverParameter(DBPDriverConstants.PARAM_SUPPORTS_TRANSACTIONS), true); + } + } + + public static class ReadOnlyFieldValidator implements IPropertyValueValidator { + @Override + public boolean isValidValue(WebExpertSettingsProperties object, Object value) throws IllegalArgumentException { + return CommonUtils.toBoolean(object.driver.getDriverParameter(DBPDriverConstants.PARAM_SUPPORTS_READ_ONLY_MODE), true); + } + } + + public static class DefaultCatalogFieldVisibleValidator implements IPropertyValueValidator { + @Override + public boolean isValidValue(WebExpertSettingsProperties object, Object value) throws IllegalArgumentException { + return CommonUtils.toBoolean(object.driver.getDriverParameter(DBPDriverConstants.PARAM_SUPPORTS_CATALOG_SELECTION), true); + } + } + + public static class DefaultSchemaFieldVisibleValidator implements IPropertyValueValidator { + @Override + public boolean isValidValue(WebExpertSettingsProperties object, Object value) throws IllegalArgumentException { + return CommonUtils.toBoolean(object.driver.getDriverParameter(DBPDriverConstants.PARAM_SUPPORTS_SCHEMA_SELECTION), true); + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebNetworkHandlerConfig.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebNetworkHandlerConfig.java index 66447daefd..6c5a390927 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebNetworkHandlerConfig.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebNetworkHandlerConfig.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -20,6 +20,8 @@ import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration; import org.jkiss.dbeaver.model.net.DBWHandlerType; import org.jkiss.dbeaver.model.net.ssh.SSHConstants; +import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; +import org.jkiss.dbeaver.registry.RegistryConstants; import org.jkiss.utils.CommonUtils; import java.util.LinkedHashMap; @@ -83,8 +85,11 @@ public Map getProperties() { @NotNull public Map getSecureProperties() { Map secureProperties = new LinkedHashMap<>(configuration.getSecureProperties()); - for (Map.Entry property : secureProperties.entrySet()) { - property.setValue(CommonUtils.isEmpty(property.getValue()) ? null : WebConnectionInfo.SECURED_VALUE); + DBPPropertyDescriptor[] descriptor = configuration.getHandlerDescriptor().getHandlerProperties(); + for (DBPPropertyDescriptor p : descriptor) { + if (p.hasFeature(RegistryConstants.ATTR_PASSWORD)) { + secureProperties.computeIfPresent(p.getId(), (k, v) -> CommonUtils.isEmpty(v) ? null : WebConnectionInfo.SECURED_VALUE); + } } return secureProperties; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebNetworkHandlerConfigInput.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebNetworkHandlerConfigInput.java index 156fc96c2e..b3f9a5c3f0 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebNetworkHandlerConfigInput.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebNetworkHandlerConfigInput.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebObjectOrigin.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebObjectOrigin.java index e93597db09..ec3e2ea407 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebObjectOrigin.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebObjectOrigin.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java index 09255e6011..292ef8601d 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -27,7 +27,6 @@ import org.jkiss.utils.ArrayUtils; import java.util.ArrayList; -import java.util.Collections; public class WebProjectInfo { private final WebSession session; @@ -54,11 +53,13 @@ public String getId() { } @Property - public boolean isGlobal() { return project.getRmProject().isGlobal(); } + public boolean isGlobal() { + return project.getRMProject().isGlobal(); + } @Property public boolean isShared() { - return project.getRmProject().isShared(); + return project.getRMProject().isShared(); } @Property @@ -73,7 +74,7 @@ public String getDescription() { @Property public boolean isCanEditDataSources() { - if (project.getRmProject().getType() == RMProjectType.USER && !customPrivateConnectionsEnabled) { + if (project.getRMProject().getType() == RMProjectType.USER && !customPrivateConnectionsEnabled) { return false; } return hasDataSourcePermission(RMProjectPermission.DATA_SOURCES_EDIT); @@ -95,12 +96,12 @@ public boolean isCanViewResources() { } private boolean hasDataSourcePermission(RMProjectPermission permission) { - return SMUtils.hasProjectPermission(session, project.getRmProject(), permission); + return SMUtils.hasProjectPermission(session, project.getRMProject(), permission); } @Property public RMResourceType[] getResourceTypes() { - RMResourceType[] resourceTypes = project.getRmProject().getResourceTypes(); + RMResourceType[] resourceTypes = project.getRMProject().getResourceTypes(); if(resourceTypes == null) { return ArrayUtils.toArray(RMResourceType.class, new ArrayList<>()); 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..1a00df3e96 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -18,6 +18,7 @@ import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.*; import org.jkiss.dbeaver.model.connection.DBPDriverConfigurationType; @@ -27,6 +28,7 @@ import org.jkiss.dbeaver.model.meta.PropertyLength; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; import org.jkiss.dbeaver.model.preferences.DBPPropertySource; +import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor; import org.jkiss.dbeaver.runtime.properties.ObjectPropertyDescriptor; import org.jkiss.utils.CommonUtils; @@ -42,6 +44,12 @@ public class WebPropertyInfo { private DBPPropertySource propertySource; private boolean showProtected; + private Object[] validValues; + + private String[] supportedConfigurationTypes = new String[0]; + + private Object defaultValue; + public WebPropertyInfo(WebSession session, DBPPropertyDescriptor property, DBPPropertySource propertySource) { this.session = session; this.property = property; @@ -88,6 +96,11 @@ public String getDescription() { } } + @Property + public String getHint() { + return property.getHint(); + } + @Property public int getOrder() { return property instanceof ObjectPropertyDescriptor ? ((ObjectPropertyDescriptor) property).getOrderNumber() : -1; @@ -116,7 +129,7 @@ public PropertyLength getLength() { @Property public Object getDefaultValue() throws DBException { - var defaultValue = property.getDefaultValue(); + var defaultValue = property.getDefaultValue() == null ? this.defaultValue : property.getDefaultValue(); return defaultValue == null ? getValue() : defaultValue; } @@ -147,9 +160,9 @@ public Object[] getValidValues() { } return validValues; } - return null; + return validValues; } - return null; + return validValues; } @Property @@ -165,7 +178,12 @@ public String[] getSupportedConfigurationTypes() { .map(DBPDriverConfigurationType::toString) .toArray(String[]::new); } - return new String[0]; + return supportedConfigurationTypes; + } + + @Property + public boolean isRequired() { + return property.isRequired(); } public boolean hasFeature(@NotNull String feature) { @@ -229,7 +247,62 @@ private Object makePropertyValue(Object value) { } return result; } + Class dataType = property.getDataType(); + if (dataType == Boolean.class || dataType == Boolean.TYPE) { + return Boolean.valueOf(value.toString()); + } return CommonUtils.toString(value); } + @Nullable + @Property + public List getScopes() { + if (property instanceof ProductSettingDescriptor productSettingDescriptor) { + return productSettingDescriptor.getScopes(); + } + return null; + } + + /** + * Returns expression for a visibility of a property. + */ + @Nullable + @Property + public List getConditions() { + if (!(property instanceof DBPConditionalProperty conditionalProperty)) { + return null; + } + List conditions = new ArrayList<>(); + String visibleExpr = conditionalProperty.getHideExpression(); + if (CommonUtils.isNotEmpty(visibleExpr)) { + conditions.add(new Condition(visibleExpr, Condition.Type.HIDE)); + } + String activeExpr = conditionalProperty.getReadOnlyExpression(); + if (CommonUtils.isNotEmpty(activeExpr)) { + conditions.add(new Condition(activeExpr, Condition.Type.READ_ONLY)); + } + return conditions; + } + + + //TODO: delete after refactoring on front-end + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + //TODO: delete after refactoring on front-end + public void setValidValues(Object[] validValues) { + this.validValues = validValues; + } + + //TODO: delete after refactoring on front-end + public void setSupportedConfigurationTypes(String[] supportedConfigurationTypes) { + this.supportedConfigurationTypes = supportedConfigurationTypes; + } + + public record Condition(@NotNull String expression, @NotNull Type conditionType) { + public enum Type { + HIDE, + READ_ONLY + } + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebSecretInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebSecretInfo.java new file mode 100644 index 0000000000..627947a358 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebSecretInfo.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model; + +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.secret.DBSSecretValue; + +public class WebSecretInfo { + private final DBSSecretValue secretValue; + + public WebSecretInfo(DBSSecretValue secretValue) { + this.secretValue = secretValue; + } + + @Property + public String getDisplayName() { + return secretValue.getDisplayName(); + } + + @Property + public String getSecretId() { + return secretValue.getUniqueId(); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebServerError.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebServerError.java index f400680375..d294bfa729 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebServerError.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebServerError.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,8 +17,8 @@ package io.cloudbeaver.model; import io.cloudbeaver.DBWebException; -import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.sql.DBQuotaException; +import org.jkiss.dbeaver.model.sql.SQLState; import java.io.PrintWriter; import java.io.StringWriter; @@ -28,22 +28,18 @@ */ public class WebServerError { - private String message; - private String stackTrace; - private String errorType; - private String errorCode; + private final String message; + private final String stackTrace; + private final String errorType; + private final String errorCode; public WebServerError(Throwable ex) { this.message = ex.getMessage(); StringWriter buf = new StringWriter(); ex.printStackTrace(new PrintWriter(buf, true)); this.stackTrace = buf.toString(); - if (ex instanceof DBException) { - errorCode = String.valueOf(((DBException) ex).getErrorCode()); - } - if (ex instanceof DBQuotaException) { - errorType = DBWebException.ERROR_CODE_QUOTA_EXCEEDED; - } + errorCode = String.valueOf(SQLState.getCodeFromException(ex)); + errorType = ex instanceof DBQuotaException ? DBWebException.ERROR_CODE_QUOTA_EXCEEDED : null; } public String getMessage() { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebServerMessage.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebServerMessage.java index 74c8b52def..a56b828902 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebServerMessage.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebServerMessage.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/apilog/ApiCallInterceptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/apilog/ApiCallInterceptor.java new file mode 100644 index 0000000000..f323842090 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/apilog/ApiCallInterceptor.java @@ -0,0 +1,45 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model.apilog; + +import jakarta.servlet.http.HttpServletRequest; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * Interceptor for API call events. + * This interface allows to intercept and log API call events in a web application. + */ +public interface ApiCallInterceptor { + + /** + * Intercept API call event. + * + */ + void onApiCallEvent( + @NotNull HttpServletRequest request, + @Nullable Map variables, + @NotNull String apiCall, + @Nullable String userId, + @NotNull LocalDateTime startTime, + @Nullable String errorMessage, + @NotNull String apiProtocol + ); +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseAuthWebAppConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseAuthWebAppConfiguration.java deleted file mode 100644 index 85c4799bfe..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseAuthWebAppConfiguration.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.model.app; - -import com.google.gson.annotations.Expose; -import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants; -import io.cloudbeaver.registry.WebAuthProviderDescriptor; -import io.cloudbeaver.registry.WebAuthProviderRegistry; -import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; -import org.jkiss.utils.ArrayUtils; - -import java.util.*; - -public abstract class BaseAuthWebAppConfiguration extends BaseWebAppConfiguration implements WebAuthConfiguration { - private String defaultAuthProvider; - private String[] enabledAuthProviders; - private final Set authConfigurations; - // Legacy auth configs, left for backward compatibility - @Expose(serialize = false) - private final Map authConfiguration; - - public BaseAuthWebAppConfiguration() { - super(); - this.defaultAuthProvider = LocalAuthProviderConstants.PROVIDER_ID; - this.enabledAuthProviders = null; - this.authConfigurations = new LinkedHashSet<>(); - this.authConfiguration = new LinkedHashMap<>(); - } - - public BaseAuthWebAppConfiguration(BaseAuthWebAppConfiguration src) { - super(src); - this.defaultAuthProvider = src.defaultAuthProvider; - this.enabledAuthProviders = src.enabledAuthProviders; - this.authConfigurations = new LinkedHashSet<>(src.authConfigurations); - this.authConfiguration = new LinkedHashMap<>(src.authConfiguration); - } - - @Override - public String getDefaultAuthProvider() { - return defaultAuthProvider; - } - - public void setDefaultAuthProvider(String defaultAuthProvider) { - this.defaultAuthProvider = defaultAuthProvider; - } - - @Override - public String[] getEnabledAuthProviders() { - if (enabledAuthProviders == null) { - // No config - enable all providers (+backward compatibility) - return WebAuthProviderRegistry.getInstance().getAuthProviders() - .stream().map(WebAuthProviderDescriptor::getId).toArray(String[]::new); - } - return enabledAuthProviders; - } - - public void setEnabledAuthProviders(String[] enabledAuthProviders) { - this.enabledAuthProviders = enabledAuthProviders; - } - - - @Override - public boolean isAuthProviderEnabled(String id) { - var authProviderDescriptor = WebAuthProviderRegistry.getInstance().getAuthProvider(id); - if (authProviderDescriptor == null) { - return false; - } - - if (!ArrayUtils.contains(getEnabledAuthProviders(), id)) { - return false; - } - if (!ArrayUtils.isEmpty(authProviderDescriptor.getRequiredFeatures())) { - for (String rf : authProviderDescriptor.getRequiredFeatures()) { - if (!isFeatureEnabled(rf)) { - return false; - } - } - } - return true; - } - - //////////////////////////////////////////// - // Auth provider configs - @Override - public Set getAuthCustomConfigurations() { - return authConfigurations; - } - - @Override - @Nullable - public SMAuthProviderCustomConfiguration getAuthProviderConfiguration(@NotNull String id) { - synchronized (authConfigurations) { - return authConfigurations.stream().filter(c -> c.getId().equals(id)).findAny().orElse(null); - } - } - - public void addAuthProviderConfiguration(@NotNull SMAuthProviderCustomConfiguration config) { - synchronized (authConfigurations) { - authConfigurations.removeIf(c -> c.getId().equals(config.getId())); - authConfigurations.add(config); - } - } - - public void setAuthProvidersConfigurations(Collection authProviders) { - synchronized (authConfigurations) { - authConfigurations.clear(); - authConfigurations.addAll(authProviders); - } - } - - public boolean deleteAuthProviderConfiguration(@NotNull String id) { - synchronized (authConfigurations) { - return authConfigurations.removeIf(c -> c.getId().equals(id)); - } - } - - public void loadLegacyCustomConfigs() { - // Convert legacy map of configs into list - if (!authConfiguration.isEmpty()) { - for (Map.Entry entry : authConfiguration.entrySet()) { - entry.getValue().setId(entry.getKey()); - authConfigurations.add(entry.getValue()); - } - authConfiguration.clear(); - } - } -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java new file mode 100644 index 0000000000..14ff58a1db --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java @@ -0,0 +1,107 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model.app; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.registry.fs.FileSystemProviderRegistry; +import org.jkiss.utils.CommonUtils; + +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Path; + +/** + * Abstract class that contains methods for loading configuration with gson. + */ +public abstract class BaseServerConfigurationController + implements ServletServerConfigurationController { + private static final Log log = Log.getLog(BaseServerConfigurationController.class); + @NotNull + private final Path homeDirectory; + + @NotNull + protected final Path workspacePath; + + protected BaseServerConfigurationController(@NotNull Path homeDirectory) { + this.homeDirectory = homeDirectory; + this.workspacePath = initWorkspacePath(); + log.debug("Workspace path initialized: " + workspacePath.toAbsolutePath()); + } + + @NotNull + public Gson getGson() { + return getGsonBuilder().create(); + } + + @NotNull + protected abstract GsonBuilder getGsonBuilder(); + + @NotNull + public abstract T getServerConfiguration(); + + @NotNull + protected synchronized Path initWorkspacePath() { + Path defaultWorkspaceLocation = homeDirectory.resolve("workspace"); + String workspaceLocation = getWorkspaceLocationFromEnv(); + if (CommonUtils.isEmpty(workspaceLocation)) { + return defaultWorkspaceLocation; + } + URI workspaceUri = URI.create(workspaceLocation); + if (workspaceUri.getScheme() == null) { + // default filesystem + return getHomeDirectory().resolve(workspaceLocation); + } else { + var externalFsProvider = + FileSystemProviderRegistry.getInstance().getFileSystemProviderBySchema(workspaceUri.getScheme()); + if (externalFsProvider == null) { + log.error("File system not found for scheme: " + workspaceUri.getScheme() + " default workspace " + + "location will be used"); + return defaultWorkspaceLocation; + } + ClassLoader fsClassloader = externalFsProvider.getInstance().getClass().getClassLoader(); + try (FileSystem externalFileSystem = FileSystems.newFileSystem(workspaceUri, + System.getenv(), + fsClassloader)) { + log.info("Path from external filesystem used for workspace: " + workspaceUri); + return externalFileSystem.provider().getPath(workspaceUri); + } catch (Exception e) { + log.error("Failed to initialize workspace path: " + workspaceUri + " default workspace " + + "location will be used", e); + } + } + return defaultWorkspaceLocation; + } + + @Nullable + protected abstract String getWorkspaceLocationFromEnv(); + + @NotNull + protected Path getHomeDirectory() { + return homeDirectory; + } + + @NotNull + @Override + public Path getWorkspacePath() { + return workspacePath; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServletApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServletApplication.java new file mode 100644 index 0000000000..7e415cc7f6 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServletApplication.java @@ -0,0 +1,282 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model.app; + +import io.cloudbeaver.model.cli.CloudBeaverInstanceServer; +import io.cloudbeaver.model.log.SLF4JLogHandler; +import org.eclipse.core.runtime.Platform; +import org.eclipse.equinox.app.IApplicationContext; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBFileController; +import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +import org.jkiss.dbeaver.model.auth.SMSessionContext; +import org.jkiss.dbeaver.model.cli.ApplicationInstanceController; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.impl.app.ApplicationRegistry; +import org.jkiss.dbeaver.model.impl.app.BaseApplicationImpl; +import org.jkiss.dbeaver.model.impl.app.BaseWorkspaceImpl; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.websocket.event.WSEventController; +import org.jkiss.dbeaver.runtime.IVariableResolver; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.dbeaver.utils.RuntimeUtils; +import org.jkiss.utils.CommonUtils; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +/** + * Servlet application + */ +public abstract class BaseServletApplication extends BaseApplicationImpl implements ServletApplication { + + public static final String DEFAULT_CONFIG_FILE_PATH = "/etc/cloudbeaver.conf"; + public static final String CUSTOM_CONFIG_FOLDER = "custom"; + public static final String CLI_PARAM_WEB_CONFIG = "-web-config"; + public static final String LOGBACK_FILE_NAME = "logback.xml"; + + private static final Log log = Log.getLog(BaseServletApplication.class); + + private String instanceId; + private CloudBeaverInstanceServer instanceServer; + @Override + public RMController createResourceController( + @NotNull SMCredentialsProvider credentialsProvider, + @NotNull DBPWorkspace workspace + ) throws DBException { + throw new IllegalStateException("Resource controller is not supported by " + getClass().getSimpleName()); + } + + @NotNull + @Override + public DBFileController createFileController(@NotNull SMCredentialsProvider credentialsProvider) { + throw new IllegalStateException("File controller is not supported by " + getClass().getSimpleName()); + } + + @Nullable + @Override + public Path getDefaultWorkingFolder() { + return getServerConfigurationController().getWorkspacePath(); + } + + @Override + public boolean isHeadlessMode() { + return true; + } + + @Override + public boolean isMultiuser() { + return true; + } + + protected boolean loadServerConfiguration() throws DBException { + Path configFilePath = getMainConfigurationFilePath().toAbsolutePath(); + + Log.setLogHandler(new SLF4JLogHandler()); + + // Load config file + log.debug("Loading configuration from " + configFilePath); + try { + getServerConfigurationController().loadServerConfiguration(configFilePath); + } catch (Exception e) { + log.error("Error parsing configuration", e); + return false; + } + + return true; + } + + @Nullable + private Path getLogbackConfigPath(Path path) { + // try to find custom logback.xml file + Path logbackConfigPath = getCustomConfigPath(path, LOGBACK_FILE_NAME); + if (Files.exists(logbackConfigPath)) { + return logbackConfigPath; + } + for (Path confFolder = path; confFolder != null; confFolder = confFolder.getParent()) { + Path lbFile = confFolder.resolve(LOGBACK_FILE_NAME); + if (Files.exists(lbFile)) { + return lbFile; + } + } + return null; + } + + public Path getLogbackConfigPath() { + Path configFilePath = getMainConfigurationFilePath().toAbsolutePath(); + Path configFolder = configFilePath.getParent(); + + // Configure logging + return getLogbackConfigPath(configFolder); + } + + protected Path getMainConfigurationFilePath() { + String configPath = DEFAULT_CONFIG_FILE_PATH; + + String[] args = Platform.getCommandLineArgs(); + for (int i = 0; i < args.length; i++) { + if (args[i].equals(CLI_PARAM_WEB_CONFIG) && args.length > i + 1) { + configPath = args[i + 1]; + break; + } + } + // try fo find custom config path (it is used mostly for docker volumes) + Path configFilePath = Path.of(configPath); + + Path customConfigPath = getCustomConfigPath(configFilePath.getParent(), configFilePath.getFileName().toString()); + if (Files.exists(customConfigPath)) { + return customConfigPath; + } + return configFilePath; + } + + @NotNull + private Path getCustomConfigPath(Path configPath, String fileName) { + var customConfigPath = configPath.resolve(CUSTOM_CONFIG_FOLDER).resolve(fileName); + return Files.exists(customConfigPath) ? customConfigPath : configPath.resolve(fileName); + } + + /** + * There is no secret controller in base web app. + * Method returns VoidSecretController instance. + * Advanced apps may implement it differently. + */ + @Override + public DBSSecretController getSecretController( + @NotNull SMCredentialsProvider credentialsProvider, + SMSessionContext smSessionContext + ) throws DBException { + return VoidSecretController.INSTANCE; + } + + public static Map getServerConfigProps(Map configProps) { + return JSONUtils.getObject(configProps, "server"); + } + + @SuppressWarnings("unchecked") + public static void patchConfigurationWithProperties( + Map configProps, IVariableResolver varResolver + ) { + for (Map.Entry entry : configProps.entrySet()) { + Object propValue = entry.getValue(); + if (propValue instanceof String) { + entry.setValue(GeneralUtils.replaceVariables((String) propValue, varResolver)); + } else if (propValue instanceof Map) { + patchConfigurationWithProperties((Map) propValue, varResolver); + } else if (propValue instanceof List) { + List value = (List) propValue; + for (int i = 0; i < value.size(); i++) { + Object colItem = value.get(i); + if (colItem instanceof String) { + value.set(i, GeneralUtils.replaceVariables((String) colItem, varResolver)); + } else if (colItem instanceof Map) { + patchConfigurationWithProperties((Map) colItem, varResolver); + } + } + } + } + } + + @Override + public Object start(IApplicationContext context) { + initializeApplicationServices(); + try { + try { + this.instanceServer = new CloudBeaverInstanceServer(); + } catch (Exception e) { + log.error("Error initializing instance server", e); + } + startServer(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + protected abstract void startServer() throws DBException; + + @Override + public synchronized String getApplicationInstanceId() throws DBException { + if (instanceId == null) { + try { + byte[] macAddress = RuntimeUtils.getLocalMacAddress(); + instanceId = String.join( + "_", + ApplicationRegistry.getInstance().getApplication().getId(), + getWorkspaceIdProperty(), // workspace id is read from property file + CommonUtils.toHexString(macAddress), + CommonUtils.toString(getServerPort()) + ); + } catch (Exception e) { + throw new DBException("Error during generation instance id generation", e); + } + } + return instanceId; + } + + @NotNull + public String getWorkspaceIdProperty() throws DBException { + return BaseWorkspaceImpl.readWorkspaceIdProperty(); + } + + @Override + public Path getWorkspaceDirectory() { + return getServerConfigurationController().getWorkspacePath(); + } + + + public String getApplicationId() { + try { + return getApplicationInstanceId(); + } catch (DBException e) { + return null; + } + } + + @Override + public WSEventController getEventController() { + return null; + } + + public abstract ServletServerConfigurationController getServerConfigurationController(); + + @Override + public boolean isEnvironmentVariablesAccessible() { + return false; + } + + protected void closeResource(String name, Runnable closeFunction) { + try { + closeFunction.run(); + } catch (Exception e) { + log.error("Failed close " + name, e); + } + } + + @Nullable + @Override + public ApplicationInstanceController getInstanceServer() { + return instanceServer; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java index 9497febe89..1f4bf33835 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -18,28 +18,36 @@ import io.cloudbeaver.DBWFeatureSet; import io.cloudbeaver.registry.WebFeatureRegistry; +import io.cloudbeaver.utils.ServletAppUtils; import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.DBConstants; import org.jkiss.utils.ArrayUtils; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; -public abstract class BaseWebAppConfiguration implements WebAppConfiguration { +public abstract class BaseWebAppConfiguration implements ServletAppConfiguration { public static final String DEFAULT_APP_ANONYMOUS_TEAM_NAME = "user"; protected final Map plugins; - protected String defaultUserTeam; + protected String defaultUserTeam = DEFAULT_APP_ANONYMOUS_TEAM_NAME; protected boolean resourceManagerEnabled; + protected boolean secretManagerEnabled; protected boolean showReadOnlyConnectionInfo; protected String[] enabledFeatures; + protected String[] disabledFeatures; + protected String[] disabledBetaFeatures; + public BaseWebAppConfiguration() { this.plugins = new LinkedHashMap<>(); - this.defaultUserTeam = DEFAULT_APP_ANONYMOUS_TEAM_NAME; this.resourceManagerEnabled = true; this.enabledFeatures = null; + this.disabledFeatures = new String[0]; + this.disabledBetaFeatures = new String[0]; this.showReadOnlyConnectionInfo = false; + this.secretManagerEnabled = false; } public BaseWebAppConfiguration(BaseWebAppConfiguration src) { @@ -47,7 +55,10 @@ public BaseWebAppConfiguration(BaseWebAppConfiguration src) { this.defaultUserTeam = src.defaultUserTeam; this.resourceManagerEnabled = src.resourceManagerEnabled; this.enabledFeatures = src.enabledFeatures; + this.disabledFeatures = src.disabledFeatures; + this.disabledBetaFeatures = src.disabledBetaFeatures; this.showReadOnlyConnectionInfo = src.showReadOnlyConnectionInfo; + this.secretManagerEnabled = src.secretManagerEnabled; } @Override @@ -81,14 +92,26 @@ public boolean isResourceManagerEnabled() { return resourceManagerEnabled; } + @Override + public boolean isSecretManagerEnabled() { + return secretManagerEnabled; + } + + @Override public boolean isFeatureEnabled(String id) { + if (DBConstants.PRODUCT_FEATURE_DISTRIBUTED.equals(id)) { + return ServletAppUtils.getServletApplication().isDistributed(); + } return ArrayUtils.contains(getEnabledFeatures(), id); } + @Override public boolean isFeaturesEnabled(String[] features) { return ArrayUtils.containsAll(getEnabledFeatures(), features); } + @NotNull + @Override public String[] getEnabledFeatures() { if (enabledFeatures == null) { // No config - enable all features (+backward compatibility) @@ -102,7 +125,21 @@ public void setEnabledFeatures(String[] enabledFeatures) { this.enabledFeatures = enabledFeatures; } + @NotNull + @Override + public String[] getDisabledFeatures() { + return disabledFeatures; + } + + public void setDisabledFeatures(@NotNull String[] disabledFeatures) { + this.disabledFeatures = disabledFeatures; + } + public boolean isShowReadOnlyConnectionInfo() { return showReadOnlyConnectionInfo; } + + public String[] getDisabledBetaFeatures() { + return disabledBetaFeatures; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java deleted file mode 100644 index 72e538dcf6..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * 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.model.app; - -import io.cloudbeaver.DataSourceFilter; -import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.WebSessionProjectImpl; -import io.cloudbeaver.model.log.SLF4JLogHandler; -import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.WebGlobalWorkspace; -import org.eclipse.core.resources.IWorkspace; -import org.eclipse.core.runtime.Platform; -import org.eclipse.equinox.app.IApplicationContext; -import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.DBFileController; -import org.jkiss.dbeaver.model.app.DBPPlatform; -import org.jkiss.dbeaver.model.app.DBPWorkspace; -import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; -import org.jkiss.dbeaver.model.data.json.JSONUtils; -import org.jkiss.dbeaver.model.impl.app.ApplicationRegistry; -import org.jkiss.dbeaver.model.rm.RMController; -import org.jkiss.dbeaver.model.rm.RMProject; -import org.jkiss.dbeaver.model.secret.DBSSecretController; -import org.jkiss.dbeaver.model.websocket.event.WSEventController; -import org.jkiss.dbeaver.registry.BaseApplicationImpl; -import org.jkiss.dbeaver.runtime.IVariableResolver; -import org.jkiss.dbeaver.utils.GeneralUtils; -import org.jkiss.dbeaver.utils.RuntimeUtils; -import org.jkiss.utils.CommonUtils; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; - -/** - * Web application - */ -public abstract class BaseWebApplication extends BaseApplicationImpl implements WebApplication { - - public static final String DEFAULT_CONFIG_FILE_PATH = "/etc/cloudbeaver.conf"; - public static final String CUSTOM_CONFIG_FOLDER = "custom"; - public static final String CLI_PARAM_WEB_CONFIG = "-web-config"; - public static final String LOGBACK_FILE_NAME = "logback.xml"; - - - private static final Log log = Log.getLog(BaseWebApplication.class); - - @NotNull - @Override - public DBPWorkspace createWorkspace(@NotNull DBPPlatform platform, @NotNull IWorkspace eclipseWorkspace) { - return new WebGlobalWorkspace(platform, eclipseWorkspace); - } - - @Override - public RMController createResourceController( - @NotNull SMCredentialsProvider credentialsProvider, - @NotNull DBPWorkspace workspace - ) throws DBException { - throw new IllegalStateException("Resource controller is not supported by " + getClass().getSimpleName()); - } - - @NotNull - @Override - public DBFileController createFileController(@NotNull SMCredentialsProvider credentialsProvider) { - throw new IllegalStateException("File controller is not supported by " + getClass().getSimpleName()); - } - - @Nullable - @Override - public Path getDefaultWorkingFolder() { - return null; - } - - @Override - public boolean isHeadlessMode() { - return true; - } - - @Override - public boolean isMultiuser() { - return true; - } - - @Nullable - protected Path loadServerConfiguration() throws DBException { - Path configFilePath = getMainConfigurationFilePath().toAbsolutePath(); - Path configFolder = configFilePath.getParent(); - - // Configure logging - Path logbackConfigPath = getLogbackConfigPath(configFolder); - - if (logbackConfigPath == null) { - System.err.println("Can't find slf4j configuration file in " + configFilePath.getParent()); - } else { - System.setProperty("logback.configurationFile", logbackConfigPath.toString()); - } - Log.setLogHandler(new SLF4JLogHandler()); - - // Load config file - log.debug("Loading configuration from " + configFilePath); - try { - loadConfiguration(configFilePath); - } catch (Exception e) { - log.error("Error parsing configuration", e); - return null; - } - - return configFilePath; - } - - @Nullable - private Path getLogbackConfigPath(Path path) { - // try to find custom logback.xml file - Path logbackConfigPath = getCustomConfigPath(path, LOGBACK_FILE_NAME); - if (Files.exists(logbackConfigPath)) { - return logbackConfigPath; - } - for (Path confFolder = path; confFolder != null; confFolder = confFolder.getParent()) { - Path lbFile = confFolder.resolve(LOGBACK_FILE_NAME); - if (Files.exists(lbFile)) { - return lbFile; - } - } - return null; - } - - private Path getMainConfigurationFilePath() { - String configPath = DEFAULT_CONFIG_FILE_PATH; - - String[] args = Platform.getCommandLineArgs(); - for (int i = 0; i < args.length; i++) { - if (args[i].equals(CLI_PARAM_WEB_CONFIG) && args.length > i + 1) { - configPath = args[i + 1]; - break; - } - } - // try fo find custom config path (it is used mostly for docker volumes) - Path configFilePath = Path.of(configPath); - - Path customConfigPath = getCustomConfigPath(configFilePath.getParent(), configFilePath.getFileName().toString()); - if (Files.exists(customConfigPath)) { - return customConfigPath; - } - return configFilePath; - } - - @NotNull - private Path getCustomConfigPath(Path configPath, String fileName) { - var customConfigPath = configPath.resolve(CUSTOM_CONFIG_FOLDER).resolve(fileName); - return Files.exists(customConfigPath) ? customConfigPath : configPath.resolve(fileName); - } - - protected abstract void loadConfiguration(Path configPath) throws DBException; - - @Override - public WebProjectImpl createProjectImpl( - @NotNull WebSession webSession, - @NotNull RMProject project, - @NotNull DataSourceFilter dataSourceFilter - ) { - return new WebSessionProjectImpl( - webSession, - project, - dataSourceFilter - ); - } - - /** - * There is no secret controller in base web app. - * Method returns VoidSecretController instance. - * Advanced apps may implement it differently. - */ - @Override - public DBSSecretController getSecretController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException { - return VoidSecretController.INSTANCE; - } - - protected Map getServerConfigProps(Map configProps) { - return JSONUtils.getObject(configProps, "server"); - } - - @SuppressWarnings("unchecked") - public static void patchConfigurationWithProperties(Map configProps, IVariableResolver varResolver) { - for (Map.Entry entry : configProps.entrySet()) { - Object propValue = entry.getValue(); - if (propValue instanceof String) { - entry.setValue(GeneralUtils.replaceVariables((String) propValue, varResolver)); - } else if (propValue instanceof Map) { - patchConfigurationWithProperties((Map) propValue, varResolver); - } else if (propValue instanceof List) { - List value = (List) propValue; - for (int i = 0; i < value.size(); i++) { - Object colItem = value.get(i); - if (colItem instanceof String) { - value.set(i, GeneralUtils.replaceVariables((String) colItem, varResolver)); - } else if (colItem instanceof Map) { - patchConfigurationWithProperties((Map) colItem, varResolver); - } - } - } - } - } - - @Override - public Object start(IApplicationContext context) { - try { - startServer(); - } catch (Exception e) { - log.error(e.getMessage(), e); - } - return null; - } - - protected abstract void startServer() throws DBException; - - @Override - public String getApplicationInstanceId() throws DBException { - try { - byte[] macAddress = RuntimeUtils.getLocalMacAddress(); - String appId = ApplicationRegistry.getInstance().getApplication().getId(); - return appId + "_" + CommonUtils.toHexString(macAddress) + getServerPort(); - } catch (Exception e) { - throw new DBException("Error during generation instance id generation", e); - } - } - - public String getApplicationId() { - try { - return getApplicationInstanceId(); - } catch (DBException e) { - return null; - } - } - - @Override - public WSEventController getEventController() { - return null; - } - -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletAppConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletAppConfiguration.java new file mode 100644 index 0000000000..e3afd69125 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletAppConfiguration.java @@ -0,0 +1,67 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.app; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; + +import java.util.Map; + +/** + * Application configuration + */ +public interface ServletAppConfiguration { + String getAnonymousUserTeam(); + + boolean isAnonymousAccessEnabled(); + + @Nullable + T getResourceQuota(String quotaId); + + String getDefaultUserTeam(); + + T getPluginOption(@NotNull String pluginId, @NotNull String option); + + Map getPluginConfig(@NotNull String pluginId, boolean create); + + boolean isResourceManagerEnabled(); + + boolean isSecretManagerEnabled(); + + boolean isFeaturesEnabled(String[] requiredFeatures); + + boolean isFeatureEnabled(String id); + + @NotNull + default String[] getEnabledFeatures() { + return new String[0]; + } + + /** + * Returns disabled features. + * + * @return array of disabled feature IDs + */ + @NotNull + default String[] getDisabledFeatures() { + return new String[0]; + } + + default boolean isSupportsCustomConnections() { + return true; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletApplication.java new file mode 100644 index 0000000000..fd8298db22 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletApplication.java @@ -0,0 +1,103 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.app; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.DBFileController; +import org.jkiss.dbeaver.model.app.DBPApplication; +import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +import org.jkiss.dbeaver.model.auth.SMSessionContext; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.security.SMAdminController; +import org.jkiss.dbeaver.model.security.SMController; +import org.jkiss.dbeaver.model.websocket.event.WSEventController; + +import java.nio.file.Path; +import java.util.Map; + +/** + * Base interface for web application + */ +public interface ServletApplication extends DBPApplication { + boolean isConfigurationMode(); + + default boolean isInitializationMode() { + return false; + } + + ServletAppConfiguration getAppConfiguration(); + + ServletServerConfiguration getServerConfiguration(); + + Path getDataDirectory(boolean create); + + Path getWorkspaceDirectory(); + + Path getHomeDirectory(); + + boolean isMultiNode(); + + SMController createSecurityController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException; + + SMAdminController getAdminSecurityController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException; + + DBSSecretController getSecretController( + @NotNull SMCredentialsProvider credentialsProvider, + SMSessionContext smSessionContext + ) throws DBException; + + RMController createResourceController( + @NotNull SMCredentialsProvider credentialsProvider, + @NotNull DBPWorkspace workspace + ) throws DBException; + + DBFileController createFileController(@NotNull SMCredentialsProvider credentialsProvider); + + String getServerURL(); + + default String getServicesURI() { + return "/"; + } + + default String getRootURI() { + return ""; + } + + String getApplicationInstanceId() throws DBException; + + WSEventController getEventController(); + + /** + * Port this server listens on + */ + int getServerPort(); + + boolean isLicenseRequired(); + + /** + * Collector that contains information about system. + */ + @NotNull + ServletSystemInformationCollector getSystemInformationCollector(); + + default void getStatusInfo(Map infoMap) { + + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletAuthApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletAuthApplication.java new file mode 100644 index 0000000000..b22f8eb7d1 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletAuthApplication.java @@ -0,0 +1,42 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model.app; + +import io.cloudbeaver.auth.CBAuthConstants; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; + +public interface ServletAuthApplication extends ServletApplication { + ServletAuthConfiguration getAuthConfiguration(); + + @Nullable + String getAuthServiceUriSegment(); + + default long getMaxSessionIdleTime() { + return CBAuthConstants.MAX_SESSION_IDLE_TIME; + } + + void flushConfiguration() throws DBException; + + String getDefaultAuthRole(); + + default String modifyOrigin(@NotNull String origin) { + return origin; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletAuthConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletAuthConfiguration.java new file mode 100644 index 0000000000..749cb51353 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletAuthConfiguration.java @@ -0,0 +1,42 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.app; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; + +import java.util.Set; + +/** + * Application authentication configuration + */ +public interface ServletAuthConfiguration { + + String getDefaultAuthProvider(); + + String[] getEnabledAuthProviders(); + + boolean isAuthProviderEnabled(String authProviderId); + + Set getAuthCustomConfigurations(); + + @Nullable + SMAuthProviderCustomConfiguration getAuthProviderConfiguration(String configId); + + void addAuthProviderConfiguration(@NotNull SMAuthProviderCustomConfiguration config); +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletServerConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletServerConfiguration.java new file mode 100644 index 0000000000..43a6e3537f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletServerConfiguration.java @@ -0,0 +1,42 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.app; + +import io.cloudbeaver.server.WebServerPreferenceStore; +import org.jkiss.code.NotNull; + +import java.util.Map; + +/** + * Web server configuration. + * Contains only server configuration properties. + */ +public interface ServletServerConfiguration { + boolean isDevelMode(); + + default String getRootURI() { + return ""; + } + + /** + * @return the setting values that will be used in {@link WebServerPreferenceStore} + */ + @NotNull + default Map getProductSettings() { + return Map.of(); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletServerConfigurationController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletServerConfigurationController.java new file mode 100644 index 0000000000..3c2afc36de --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletServerConfigurationController.java @@ -0,0 +1,49 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.app; + +import com.google.gson.Gson; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; + +import java.nio.file.Path; +import java.util.Map; + +/** + * Server configuration controller. + * Works with app server configuration (loads, updates) + */ +public interface ServletServerConfigurationController { + + /** + * Loads server configuration. + */ + void loadServerConfiguration(Path configPath) throws DBException; + + @NotNull + default Map getOriginalConfigurationProperties() { + return Map.of(); + } + + @NotNull + Path getWorkspacePath(); + + @NotNull + Gson getGson(); + + void validateFinalServerConfiguration() throws DBException; +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletSystemInformationCollector.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletSystemInformationCollector.java new file mode 100644 index 0000000000..d6e5769e83 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/ServletSystemInformationCollector.java @@ -0,0 +1,170 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.app; + +import io.cloudbeaver.auth.NoAuthCredentialsProvider; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.DBPConnectionInformation; +import org.jkiss.dbeaver.model.DBPObject; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.meta.PropertyGroup; +import org.jkiss.dbeaver.model.meta.PropertyLength; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.dbeaver.utils.SystemVariablesResolver; +import org.jkiss.utils.StandardConstants; + +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Web system information collector. + */ +public class ServletSystemInformationCollector implements DBPObject { + + private enum DeploymentType { + DEFAULT, + DOCKER, + KUBERNETES + } + + @NotNull + protected final T application; + + @NotNull + private final String osInfo; + @NotNull + private final String javaVersion; + @NotNull + private final String javaParameters; + @NotNull + private final String productName; + @NotNull + private final String productVersion; + @NotNull + private final String memoryAvailable; + @NotNull + private final String installPath; + private DBPConnectionInformation smDatabaseInfo; + private String workspacePath; + private final DeploymentType deploymentType; + + public ServletSystemInformationCollector(@NotNull T application) { + this.application = application; + this.osInfo = System.getProperty(StandardConstants.ENV_OS_NAME) + " " + System.getProperty( + StandardConstants.ENV_OS_VERSION) + " (" + System.getProperty(StandardConstants.ENV_OS_ARCH) + ")"; + this.javaVersion = System.getProperty(StandardConstants.ENV_JAVA_VERSION) + " by " + System.getProperty( + StandardConstants.ENV_JAVA_VENDOR) + " (" + System.getProperty(StandardConstants.ENV_JAVA_ARCH) + "bit)"; + this.javaParameters = System.getProperty("sun.java.command"); + this.productName = GeneralUtils.getProductName(); + this.productVersion = GeneralUtils.getProductVersion().toString(); + this.installPath = SystemVariablesResolver.getInstallPath(); + this.memoryAvailable = "%dMb/%dMb".formatted( + Runtime.getRuntime().totalMemory() / (1024 * 1024), + Runtime.getRuntime().maxMemory() / (1024 * 1024) + ); + deploymentType = checkDeploymentType(); + } + + private DeploymentType checkDeploymentType() { + if (System.getenv("KUBERNETES_SERVICE_HOST") != null) { + return DeploymentType.KUBERNETES; + } + if (isRunningInDocker()) { + return DeploymentType.DOCKER; + } + + return DeploymentType.DEFAULT; + } + + @NotNull + @Property(order = 1) + public String getProductName() { + return productName; + } + + @NotNull + @Property(order = 2) + public String getProductVersion() { + return productVersion; + } + + @NotNull + @Property(order = 11) + public String getOsInfo() { + return osInfo; + } + + @NotNull + @Property(order = 12) + public String getMemoryAvailable() { + return memoryAvailable; + } + + @NotNull + @Property(order = 21, length = PropertyLength.MULTILINE) + public String getJavaVersion() { + return javaVersion; + } + + @NotNull + @Property(order = 22, length = PropertyLength.MULTILINE) + public String getJavaParameters() { + return javaParameters; + } + + @NotNull + @Property(order = 23) + public String getDeploymentType() { + return deploymentType.name(); + } + + @NotNull + @PropertyGroup(order = 31, category = "Security manager database", groupId = "sm") + public DBPConnectionInformation getSmDatabaseInfo() { + return smDatabaseInfo; + } + + @Property(order = Integer.MAX_VALUE - 10, length = PropertyLength.MULTILINE) + public String getWorkspacePath() { + return workspacePath; + } + + public void setWorkspacePath(String workspacePath) { + this.workspacePath = workspacePath; + } + + @NotNull + @Property(order = Integer.MAX_VALUE - 10, length = PropertyLength.MULTILINE) + public String getInstallPath() { + return installPath; + } + + + /** + * Collects info about internal databases. + */ + public void collectInternalDatabaseUseInformation() throws DBException { + this.smDatabaseInfo = application.getAdminSecurityController(new NoAuthCredentialsProvider()) + .getInternalDatabaseInformation(); + } + + private static boolean isRunningInDocker() { + Path cgroupPath = Path.of("/.dockerenv"); + return Files.exists(cgroupPath); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/VoidSecretController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/VoidSecretController.java index c510b9ad44..f3fa170639 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/VoidSecretController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/VoidSecretController.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -19,12 +19,20 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +import org.jkiss.dbeaver.model.auth.SMSessionContext; +import org.jkiss.dbeaver.model.exec.DBCFeatureNotSupportedException; import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.secret.DBSSecretControllerAuthorized; +import org.jkiss.dbeaver.model.secret.DBSSecretObject; +import org.jkiss.dbeaver.model.secret.DBSSecretValue; + +import java.util.List; /** * Void secret controller. */ -public class VoidSecretController implements DBSSecretController { +public class VoidSecretController implements DBSSecretController, DBSSecretControllerAuthorized { public static final VoidSecretController INSTANCE = new VoidSecretController(); @@ -34,17 +42,30 @@ public VoidSecretController() { @Nullable @Override - public String getSecretValue(@NotNull String secretId) { + public String getPrivateSecretValue(@NotNull String secretId) { return null; } @Override - public void setSecretValue(@NotNull String secretId, @Nullable String secretValue) throws DBException { + public void setPrivateSecretValue(@NotNull String secretId, @Nullable String secretValue) throws DBException { throw new DBException("Secret controller is read-only"); } + @NotNull + @Override + public List discoverCurrentUserSecrets(@NotNull DBSSecretObject secretObject) throws DBException { + throw new DBCFeatureNotSupportedException("Secrets discovery not supported"); + } + @Override public void flushChanges() { } + @Override + public void authorize( + @Nullable SMCredentialsProvider credentialsProvider, + @Nullable SMSessionContext smSessionContext + ) throws DBException { + + } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java index 1a5f960ce6..fefe5225f6 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,32 +17,38 @@ package io.cloudbeaver.model.app; import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; +import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings; import java.util.Map; /** * Application configuration */ -public interface WebAppConfiguration { - String getAnonymousUserTeam(); +public interface WebAppConfiguration extends ServletAppConfiguration { + DataSourceNavigatorSettings.Preset PRESET_WEB = new DataSourceNavigatorSettings.Preset("web", + "Web", + "Default view"); - boolean isAnonymousAccessEnabled(); + DBNBrowseSettings getDefaultNavigatorSettings(); - T getResourceQuota(String quotaId); + boolean isPublicCredentialsSaveEnabled(); - String getDefaultUserTeam(); + boolean isAdminCredentialsSaveEnabled(); - T getPluginOption(@NotNull String pluginId, @NotNull String option); - - Map getPluginConfig(@NotNull String pluginId, boolean create); + default String[] getDisabledBetaFeatures() { + return new String[0]; + } - boolean isResourceManagerEnabled(); + default String[] getEnabledAuthProviders() { + return new String[0]; + } - boolean isFeaturesEnabled(String[] requiredFeatures); + @NotNull + String[] getEnabledDrivers(); - boolean isFeatureEnabled(String id); + @NotNull + String[] getDisabledDrivers(); - default boolean isSupportsCustomConnections() { - return true; - } + Map getResourceQuotas(); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java deleted file mode 100644 index 0b06478792..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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.model.app; - -import io.cloudbeaver.DataSourceFilter; -import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.model.session.WebSession; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.DBFileController; -import org.jkiss.dbeaver.model.app.DBPApplication; -import org.jkiss.dbeaver.model.app.DBPWorkspace; -import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; -import org.jkiss.dbeaver.model.rm.RMController; -import org.jkiss.dbeaver.model.rm.RMProject; -import org.jkiss.dbeaver.model.secret.DBSSecretController; -import org.jkiss.dbeaver.model.security.SMAdminController; -import org.jkiss.dbeaver.model.security.SMController; -import org.jkiss.dbeaver.model.websocket.event.WSEventController; - -import java.nio.file.Path; - -/** - * Base interface for web application - */ -public interface WebApplication extends DBPApplication { - boolean isConfigurationMode(); - - WebAppConfiguration getAppConfiguration(); - - Path getDataDirectory(boolean create); - - Path getWorkspaceDirectory(); - - Path getHomeDirectory(); - - boolean isMultiNode(); - - WebProjectImpl createProjectImpl( - @NotNull WebSession webSession, - @NotNull RMProject project, - @NotNull DataSourceFilter dataSourceFilter - ); - - SMController createSecurityController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException; - - SMAdminController getAdminSecurityController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException; - - DBSSecretController getSecretController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException; - - RMController createResourceController( - @NotNull SMCredentialsProvider credentialsProvider, - @NotNull DBPWorkspace workspace - ) throws DBException; - - DBFileController createFileController(@NotNull SMCredentialsProvider credentialsProvider); - - String getServerURL(); - - default String getServicesURI() { - return "/"; - } - - default String getRootURI() { - return ""; - } - - String getApplicationInstanceId() throws DBException; - - WSEventController getEventController(); - - /** - * Port this server listens on - */ - int getServerPort(); - - boolean isLicenseRequired(); -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthApplication.java deleted file mode 100644 index 8ac69c9f3f..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthApplication.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.model.app; - -import io.cloudbeaver.auth.CBAuthConstants; - -public interface WebAuthApplication extends WebApplication { - WebAuthConfiguration getAuthConfiguration(); - - String getAuthServiceURL(); - - default long getMaxSessionIdleTime() { - return CBAuthConstants.MAX_SESSION_IDLE_TIME; - } -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthConfiguration.java deleted file mode 100644 index 930b2d5efd..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAuthConfiguration.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.model.app; - -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; - -import java.util.Set; - -/** - * Application authentication configuration - */ -public interface WebAuthConfiguration { - - String getDefaultAuthProvider(); - - String[] getEnabledAuthProviders(); - - boolean isAuthProviderEnabled(String authProviderId); - - Set getAuthCustomConfigurations(); - - @Nullable - SMAuthProviderCustomConfiguration getAuthProviderConfiguration(String configId); -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/cli/CloudBeaverCommandLine.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/cli/CloudBeaverCommandLine.java new file mode 100644 index 0000000000..177f711a3a --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/cli/CloudBeaverCommandLine.java @@ -0,0 +1,35 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model.cli; + +import org.jkiss.dbeaver.model.cli.ApplicationCommandLine; +import org.jkiss.dbeaver.model.cli.ApplicationInstanceController; + +public class CloudBeaverCommandLine extends ApplicationCommandLine { + private static CloudBeaverCommandLine INSTANCE = null; + + private CloudBeaverCommandLine() { + super(); + } + + public synchronized static CloudBeaverCommandLine getInstance() { + if (INSTANCE == null) { + INSTANCE = new CloudBeaverCommandLine(); + } + return INSTANCE; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/cli/CloudBeaverInstanceServer.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/cli/CloudBeaverInstanceServer.java new file mode 100644 index 0000000000..a65e869b1f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/cli/CloudBeaverInstanceServer.java @@ -0,0 +1,46 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model.cli; + +import org.apache.commons.cli.CommandLine; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.cli.ApplicationInstanceController; +import org.jkiss.dbeaver.model.cli.ApplicationInstanceServer; +import org.jkiss.dbeaver.model.cli.CliProcessResult; + +import java.io.IOException; + +public class CloudBeaverInstanceServer extends ApplicationInstanceServer { + public CloudBeaverInstanceServer() throws IOException { + super(ApplicationInstanceController.class); + } + + @NotNull + @Override + public CliProcessResult handleCommandLine(@NotNull String[] args) { + CommandLine cmd = CloudBeaverCommandLine.getInstance().getCommandLine(args); + try { + return CloudBeaverCommandLine.getInstance().executeCommandLineCommands( + cmd, + this, + false + ); + } catch (Exception e) { + return new CliProcessResult(CliProcessResult.PostAction.ERROR, "Error executing command: " + e.getMessage()); + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/cli/CloudbeaverCliConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/cli/CloudbeaverCliConstants.java new file mode 100644 index 0000000000..a14d14f464 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/cli/CloudbeaverCliConstants.java @@ -0,0 +1,21 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model.cli; + +public interface CloudbeaverCliConstants { + String CLI_MODE = "cliMode"; +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/CBAppConfig.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/CBAppConfig.java new file mode 100644 index 0000000000..5f73d3ebee --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/CBAppConfig.java @@ -0,0 +1,357 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.config; + +import com.google.gson.annotations.Expose; +import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants; +import io.cloudbeaver.model.app.BaseWebAppConfiguration; +import io.cloudbeaver.model.app.ServletAuthConfiguration; +import io.cloudbeaver.model.app.WebAppConfiguration; +import io.cloudbeaver.registry.WebAuthProviderDescriptor; +import io.cloudbeaver.registry.WebAuthProviderRegistry; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; +import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings; +import org.jkiss.utils.ArrayUtils; +import org.jkiss.utils.CommonUtils; + +import java.util.*; + +/** + * Application configuration + */ +public class CBAppConfig extends BaseWebAppConfiguration implements ServletAuthConfiguration, WebAppConfiguration { + private static final Log log = Log.getLog(CBAppConfig.class); + + public static final DataSourceNavigatorSettings DEFAULT_VIEW_SETTINGS = PRESET_WEB.getSettings(); + private final Set authConfigurations; + // Legacy auth configs, left for backward compatibility + @Expose(serialize = false) + private final Map authConfiguration; + + private boolean supportsCustomConnections; + private boolean enableReverseProxyAuth; + private boolean forwardProxy; + private boolean publicCredentialsSaveEnabled; + private boolean adminCredentialsSaveEnabled; + private boolean linkExternalCredentialsWithUser; + + private boolean redirectOnFederatedAuth; + private boolean anonymousAccessEnabled; + private boolean grantConnectionsAccessToAnonymousTeam; + private boolean systemVariablesResolvingEnabled; + @Deprecated + private String anonymousUserRole; + private String anonymousUserTeam; + + private String[] enabledDrivers; + private String[] disabledDrivers; + private DataSourceNavigatorSettings defaultNavigatorSettings; + + private final Map resourceQuotas; + private String defaultAuthProvider; + private String[] enabledAuthProviders; + + public CBAppConfig() { + this.defaultAuthProvider = LocalAuthProviderConstants.PROVIDER_ID; + this.enabledAuthProviders = null; + this.authConfigurations = new LinkedHashSet<>(); + this.authConfiguration = new LinkedHashMap<>(); + this.anonymousAccessEnabled = false; + this.anonymousUserRole = DEFAULT_APP_ANONYMOUS_TEAM_NAME; + this.anonymousUserTeam = DEFAULT_APP_ANONYMOUS_TEAM_NAME; + this.supportsCustomConnections = true; + this.publicCredentialsSaveEnabled = true; + this.adminCredentialsSaveEnabled = true; + this.redirectOnFederatedAuth = false; + this.enabledDrivers = new String[0]; + this.disabledDrivers = new String[0]; + this.defaultNavigatorSettings = DEFAULT_VIEW_SETTINGS; + this.resourceQuotas = new LinkedHashMap<>(); + this.enableReverseProxyAuth = false; + this.forwardProxy = false; + this.linkExternalCredentialsWithUser = true; + this.grantConnectionsAccessToAnonymousTeam = false; + this.systemVariablesResolvingEnabled = false; + } + + public CBAppConfig(CBAppConfig src) { + super(src); + this.defaultAuthProvider = src.defaultAuthProvider; + this.enabledAuthProviders = src.enabledAuthProviders; + this.authConfigurations = new LinkedHashSet<>(src.authConfigurations); + this.authConfiguration = new LinkedHashMap<>(src.authConfiguration); + this.anonymousAccessEnabled = src.anonymousAccessEnabled; + this.anonymousUserRole = src.anonymousUserRole; + this.anonymousUserTeam = src.anonymousUserTeam; + this.supportsCustomConnections = src.supportsCustomConnections; + this.publicCredentialsSaveEnabled = src.publicCredentialsSaveEnabled; + this.adminCredentialsSaveEnabled = src.adminCredentialsSaveEnabled; + this.redirectOnFederatedAuth = src.redirectOnFederatedAuth; + this.enabledDrivers = src.enabledDrivers; + this.disabledDrivers = src.disabledDrivers; + this.defaultNavigatorSettings = src.defaultNavigatorSettings; + this.resourceQuotas = new LinkedHashMap<>(src.resourceQuotas); + this.enableReverseProxyAuth = src.enableReverseProxyAuth; + this.forwardProxy = src.forwardProxy; + this.linkExternalCredentialsWithUser = src.linkExternalCredentialsWithUser; + this.grantConnectionsAccessToAnonymousTeam = src.grantConnectionsAccessToAnonymousTeam; + this.systemVariablesResolvingEnabled = src.systemVariablesResolvingEnabled; + } + + @Override + public boolean isAnonymousAccessEnabled() { + return anonymousAccessEnabled; + } + + @Override + public String getAnonymousUserTeam() { + return CommonUtils.notNull(anonymousUserTeam, anonymousUserRole); + } + + public void setAnonymousAccessEnabled(boolean anonymousAccessEnabled) { + this.anonymousAccessEnabled = anonymousAccessEnabled; + } + + public void setResourceManagerEnabled(boolean resourceManagerEnabled) { + this.resourceManagerEnabled = resourceManagerEnabled; + } + + public void setSecretManagerEnabled(boolean secretManagerEnabled) { + this.secretManagerEnabled = secretManagerEnabled; + } + + public boolean isSupportsCustomConnections() { + return supportsCustomConnections; + } + + public void setSupportsCustomConnections(boolean supportsCustomConnections) { + this.supportsCustomConnections = supportsCustomConnections; + } + + public boolean isPublicCredentialsSaveEnabled() { + return publicCredentialsSaveEnabled; + } + + public void setPublicCredentialsSaveEnabled(boolean publicCredentialsSaveEnabled) { + this.publicCredentialsSaveEnabled = publicCredentialsSaveEnabled; + } + + public boolean isAdminCredentialsSaveEnabled() { + return adminCredentialsSaveEnabled; + } + + public void setAdminCredentialsSaveEnabled(boolean adminCredentialsSaveEnabled) { + this.adminCredentialsSaveEnabled = adminCredentialsSaveEnabled; + } + + public boolean isRedirectOnFederatedAuth() { + return redirectOnFederatedAuth; + } + + @NotNull + public String[] getEnabledDrivers() { + return enabledDrivers; + } + + public void setEnabledDrivers(String[] enabledDrivers) { + this.enabledDrivers = enabledDrivers; + } + + @NotNull + public String[] getDisabledDrivers() { + return disabledDrivers; + } + + public void setDisabledDrivers(String[] disabledDrivers) { + this.disabledDrivers = disabledDrivers; + } + + public String[] getAllAuthProviders() { + return WebAuthProviderRegistry.getInstance().getAuthProviders() + .stream().map(WebAuthProviderDescriptor::getId).toArray(String[]::new); + } + + public DBNBrowseSettings getDefaultNavigatorSettings() { + return defaultNavigatorSettings; + } + + public void setDefaultNavigatorSettings(DBNBrowseSettings defaultNavigatorSettings) { + this.defaultNavigatorSettings = new DataSourceNavigatorSettings(defaultNavigatorSettings); + } + + @NotNull + public Map getPlugins() { + return plugins; + } + + public void setPlugins(@NotNull Map plugins) { + this.plugins.clear(); + this.plugins.putAll(plugins); + } + + public Map getPluginConfig(@NotNull String pluginId) { + return getPluginConfig(pluginId, false); + } + + //////////////////////////////////////////// + // Quotas + + public Map getResourceQuotas() { + return resourceQuotas; + } + + public T getResourceQuota(String quotaId) { + Object quota = resourceQuotas.get(quotaId); + if (quota instanceof String) { + quota = CommonUtils.toDouble(quota); + } + return (T) quota; + } + + public T getResourceQuota(String quotaId, T defaultValue) { + if (resourceQuotas.containsKey(quotaId)) { + return (T) getResourceQuota(quotaId); + } else { + return defaultValue; + } + } + + public boolean isLinkExternalCredentialsWithUser() { + return linkExternalCredentialsWithUser; + } + + + //////////////////////////////////////////// + // Reverse proxy auth + + public boolean isEnabledReverseProxyAuth() { + return enableReverseProxyAuth; + } + + //////////////////////////////////////////// + // Forward proxy + + public boolean isEnabledForwardProxy() { + return forwardProxy; + } + + + public boolean isGrantConnectionsAccessToAnonymousTeam() { + return grantConnectionsAccessToAnonymousTeam; + } + + public boolean isDriverForceEnabled(@NotNull String driverId) { + return ArrayUtils.containsIgnoreCase(getEnabledDrivers(), driverId); + } + + public boolean isSystemVariablesResolvingEnabled() { + return systemVariablesResolvingEnabled; + } + + @Override + public String getDefaultAuthProvider() { + return defaultAuthProvider; + } + + public void setDefaultAuthProvider(String defaultAuthProvider) { + this.defaultAuthProvider = defaultAuthProvider; + } + + @Override + public String[] getEnabledAuthProviders() { + if (enabledAuthProviders == null) { + // No config - enable all providers (+backward compatibility) + return WebAuthProviderRegistry.getInstance().getAuthProviders() + .stream().map(WebAuthProviderDescriptor::getId).toArray(String[]::new); + } + return enabledAuthProviders; + } + + public void setEnabledAuthProviders(String[] enabledAuthProviders) { + this.enabledAuthProviders = enabledAuthProviders; + } + + @Override + public boolean isAuthProviderEnabled(String id) { + var authProviderDescriptor = WebAuthProviderRegistry.getInstance().getAuthProvider(id); + if (authProviderDescriptor == null) { + return false; + } + + if (!ArrayUtils.contains(getEnabledAuthProviders(), id)) { + return false; + } + if (!ArrayUtils.isEmpty(authProviderDescriptor.getRequiredFeatures())) { + for (String rf : authProviderDescriptor.getRequiredFeatures()) { + if (!isFeatureEnabled(rf)) { + return false; + } + } + } + return true; + } + + //////////////////////////////////////////// + // Auth provider configs + @Override + public Set getAuthCustomConfigurations() { + return authConfigurations; + } + + @Override + @Nullable + public SMAuthProviderCustomConfiguration getAuthProviderConfiguration(@NotNull String id) { + synchronized (authConfigurations) { + return authConfigurations.stream().filter(c -> c.getId().equals(id)).findAny().orElse(null); + } + } + + public void addAuthProviderConfiguration(@NotNull SMAuthProviderCustomConfiguration config) { + synchronized (authConfigurations) { + authConfigurations.removeIf(c -> c.getId().equals(config.getId())); + authConfigurations.add(config); + } + } + + public void setAuthProvidersConfigurations(Collection authProviders) { + synchronized (authConfigurations) { + authConfigurations.clear(); + authConfigurations.addAll(authProviders); + } + } + + public boolean deleteAuthProviderConfiguration(@NotNull String id) { + synchronized (authConfigurations) { + return authConfigurations.removeIf(c -> c.getId().equals(id)); + } + } + + public void loadLegacyCustomConfigs() { + // Convert legacy map of configs into list + if (!authConfiguration.isEmpty()) { + for (Map.Entry entry : authConfiguration.entrySet()) { + entry.getValue().setId(entry.getKey()); + authConfigurations.add(entry.getValue()); + } + authConfiguration.clear(); + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/ConnectionBruteForceConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/ConnectionBruteForceConfiguration.java new file mode 100644 index 0000000000..fbd38458e2 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/ConnectionBruteForceConfiguration.java @@ -0,0 +1,62 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model.config; + +/** + * Connection brute force configuration. + */ +public class ConnectionBruteForceConfiguration { + private static final int DEFAULT_ERROR_ATTEMPTS_CHECK_MINUTES = 24 * 60; + private static final int BLOCK_TIME_IN_MINUTES = 60; + private static final int DEFAULT_MAX_CONNECT_ATTEMPTS = 5; + private boolean enabled = false; + private int maxFailedConnectAttempts = DEFAULT_MAX_CONNECT_ATTEMPTS; + private int errorAttemptsPeriodInMinutes = DEFAULT_ERROR_ATTEMPTS_CHECK_MINUTES; + private int blockTimeInMinutes = BLOCK_TIME_IN_MINUTES; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getMaxFailedConnectAttempts() { + return maxFailedConnectAttempts; + } + + public void setMaxFailedConnectAttempts(int maxFailedConnectAttempts) { + this.maxFailedConnectAttempts = maxFailedConnectAttempts; + } + + public int getErrorAttemptsPeriodInMinutes() { + return errorAttemptsPeriodInMinutes; + } + + public void setErrorAttemptsPeriodInMinutes(int errorAttemptsPeriodInMinutes) { + this.errorAttemptsPeriodInMinutes = errorAttemptsPeriodInMinutes; + } + + public int getBlockTimeInMinutes() { + return blockTimeInMinutes; + } + + public void setBlockTimeInMinutes(int blockTimeInMinutes) { + this.blockTimeInMinutes = blockTimeInMinutes; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/PasswordPolicyConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/PasswordPolicyConfiguration.java new file mode 100644 index 0000000000..fb5e674afa --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/PasswordPolicyConfiguration.java @@ -0,0 +1,50 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.config; + +import org.jkiss.dbeaver.model.meta.Property; + +public class PasswordPolicyConfiguration { + private static final int DEFAULT_MIN_LENGTH = 8; + private static final int DEFAULT_MIN_DIGITS = 1; + private static final int DEFAULT_MIN_SPECIAL_CHARACTERS = 0; + private static final boolean DEFAULT_REQUIRES_UPPER_LOWER_CASE = true; + private int minLength = DEFAULT_MIN_LENGTH; + private int minNumberCount = DEFAULT_MIN_DIGITS; + private int minSymbolCount = DEFAULT_MIN_SPECIAL_CHARACTERS; + private boolean requireMixedCase = DEFAULT_REQUIRES_UPPER_LOWER_CASE; + + @Property + public int getMinLength() { + return minLength; + } + + @Property + public int getMinNumberCount() { + return minNumberCount; + } + + @Property + public int getMinSymbolCount() { + return minSymbolCount; + } + + @Property + public boolean isRequireMixedCase() { + return requireMixedCase; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/SMControllerConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/SMControllerConfiguration.java new file mode 100644 index 0000000000..ad22bd5736 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/SMControllerConfiguration.java @@ -0,0 +1,100 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.config; + +public class SMControllerConfiguration { + //in minutes + public static final int DEFAULT_ACCESS_TOKEN_TTL = 20; + public static final int DEFAULT_REFRESH_TOKEN_TTL = 4320; //72h + public static final int DEFAULT_EXPIRED_AUTH_ATTEMPT_INFO_TTL = 60; //72h + + private int accessTokenTtl = DEFAULT_ACCESS_TOKEN_TTL; + private int refreshTokenTtl = DEFAULT_REFRESH_TOKEN_TTL; + private int expiredAuthAttemptInfoTtl = DEFAULT_EXPIRED_AUTH_ATTEMPT_INFO_TTL; + + private boolean enableBruteForceProtection = true; + + //in seconds + public static final int DEFAULT_MAX_FAILED_LOGIN = 10; + public static final int DEFAULT_MINIMUM_LOGIN_TIMEOUT = 1; //1sec + public static final int DEFAULT_BLOCK_LOGIN_PERIOD = 300; //5min + private int maxFailedLogin = DEFAULT_MAX_FAILED_LOGIN; + private int minimumLoginTimeout = DEFAULT_MINIMUM_LOGIN_TIMEOUT; + private int blockLoginPeriod = DEFAULT_BLOCK_LOGIN_PERIOD; + private final PasswordPolicyConfiguration passwordPolicy = new PasswordPolicyConfiguration(); + + public int getAccessTokenTtl() { + return accessTokenTtl; + } + + public void setAccessTokenTtl(int accessTokenTtl) { + this.accessTokenTtl = accessTokenTtl; + } + + public int getRefreshTokenTtl() { + return refreshTokenTtl; + } + + public void setRefreshTokenTtl(int refreshTokenTtl) { + this.refreshTokenTtl = refreshTokenTtl; + } + + public int getExpiredAuthAttemptInfoTtl() { + return expiredAuthAttemptInfoTtl; + } + + public void setExpiredAuthAttemptInfoTtl(int expiredAuthAttemptInfoTtl) { + this.expiredAuthAttemptInfoTtl = expiredAuthAttemptInfoTtl; + } + + public void setCheckBruteforce(boolean checkBruteforce) { + this.enableBruteForceProtection = checkBruteforce; + } + + public boolean isCheckBruteforce() { + return enableBruteForceProtection; + } + + public int getMaxFailedLogin() { + return maxFailedLogin; + } + + public int getMinimumLoginTimeout() { + return minimumLoginTimeout; + } + + public int getBlockLoginPeriod() { + return blockLoginPeriod; + } + + public void setMaxFailedLogin(int maxFailed) { + this.maxFailedLogin = maxFailed; + } + + public void setMinimumLoginTimeout(int minimumTimeout) { + this.minimumLoginTimeout = minimumTimeout; + } + + public void setBlockLoginPeriod(int blockPeriod) { + this.blockLoginPeriod = blockPeriod; + } + + public PasswordPolicyConfiguration getPasswordPolicyConfiguration() { + return passwordPolicy; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/WebDatabaseConfig.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/WebDatabaseConfig.java new file mode 100644 index 0000000000..8a5ef39010 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/WebDatabaseConfig.java @@ -0,0 +1,100 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.config; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.connection.InternalDatabaseConfig; + +/** + * Database configuration + */ +public class WebDatabaseConfig implements InternalDatabaseConfig { + private String driver; + private String url; + private String user; + private String password; + private String schema; + + private String initialDataConfiguration; + + private boolean backupEnabled; + + private final Pool pool = new Pool(); + + @Override + public String getDriver() { + return driver; + } + + public void setDriver(String driver) { + this.driver = driver; + } + + @Override + @NotNull + public String getUrl() { + return url; + } + + public void setUrl(@NotNull String url) { + this.url = url; + } + + public void setBackupEnabled(boolean backupEnabled) { + this.backupEnabled = backupEnabled; + } + + @Override + public String getUser() { + return user; + } + + + @Override + public String getPassword() { + return password; + } + + public String getInitialDataConfiguration() { + return initialDataConfiguration; + } + + public Pool getPool() { + return pool; + } + + @Override + public boolean isBackupEnabled() { + return backupEnabled; + } + + public String getSchema() { + return schema; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public void setUser(String user) { + this.user = user; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java new file mode 100644 index 0000000000..ca64c38f36 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java @@ -0,0 +1,58 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.fs; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystem; +import org.jkiss.dbeaver.model.navigator.DBNModel; +import org.jkiss.dbeaver.model.navigator.DBNNode; +import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; + +import java.nio.file.Path; + +public class FSUtils { + @NotNull + public static String makeUniqueFsId(@NotNull DBFVirtualFileSystem fileSystem) { + return fileSystem.getType() + "://" + fileSystem.getId(); + } + + @NotNull + public static Path getPathFromNode(@NotNull WebSession webSession, @NotNull String nodePath) throws DBException { + DBNPathBase dbnPath = getNodeByPath(webSession, nodePath); + Path path = dbnPath.getPath(); + if (path == null) { + throw new DBWebException("Path from node '" + nodePath + "' is empty"); + } + return path; + } + + @NotNull + public static DBNPathBase getNodeByPath(@NotNull WebSession webSession, @NotNull String nodePath) throws DBException { + DBRProgressMonitor monitor = webSession.getProgressMonitor(); + + DBNModel navigatorModel = webSession.getNavigatorModelOrThrow(); + DBNNode node = navigatorModel.getNodeByPath(monitor, nodePath); + if (!(node instanceof DBNPathBase dbnPath)) { + throw new DBWebException("Node '" + nodePath + "' is not found in File Systems"); + } + return dbnPath; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java index 0508634d97..d934abf113 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java @@ -1,18 +1,18 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp + * Copyright (C) 2010-2024 DBeaver Corp and others * - * All Rights Reserved. + * 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 * - * 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. + * 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.model.rm; @@ -20,7 +20,6 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBIcon; import org.jkiss.dbeaver.model.DBPImage; import org.jkiss.dbeaver.model.DBPObject; @@ -37,7 +36,6 @@ import java.util.List; public class DBNResourceManagerProject extends DBNAbstractResourceManagerNode { - private static final Log log = Log.getLog(DBNResourceManagerProject.class); private final RMProject project; @@ -56,19 +54,25 @@ public String getName() { return project.getId(); } + @NotNull + @Override + public String getNodeId() { + return project.getId(); + } + @Override public String getNodeType() { return "rm.project"; } @Override - public String getNodeName() { + public String getNodeDisplayName() { return project.getDisplayName(); } @Override public String getLocalizedName(String locale) { - return getNodeName(); + return getNodeDisplayName(); } @Override @@ -87,8 +91,8 @@ protected boolean allowsChildren() { } @Override - public DBNResourceManagerResource[] getChildren(DBRProgressMonitor monitor) throws DBException { - if (children == null) { + public DBNResourceManagerResource[] getChildren(@NotNull DBRProgressMonitor monitor) throws DBException { + if (children == null && !monitor.isForceCacheUsage()) { List rfList = new ArrayList<>(); for (RMResource resource : getResourceController().listResources( project.getId(), null, null, true, false, false)) { @@ -105,6 +109,7 @@ protected RMController getResourceController() { return ((DBNResourceManagerRoot) getParentNode()).getResourceController(); } + @Deprecated @Override public String getNodeItemPath() { return getParentNode().getNodeItemPath() + "/" + getName(); @@ -116,21 +121,15 @@ public DBNNode refreshNode(DBRProgressMonitor monitor, Object source) throws DBE return this; } + @Nullable @Override - public String toString() { - return getNodeName(); - } - - - @Override - public DBPProject getOwnerProject() { + public DBPProject getOwnerProjectOrNull() { List globalProjects = getModel().getModelProjects(); - if (globalProjects == null) { - return null; - } - for (DBPProject modelProject : globalProjects) { - if (CommonUtils.equalObjects(modelProject.getId(), project.getId())) { - return modelProject; + if (globalProjects != null) { + for (DBPProject modelProject : globalProjects) { + if (CommonUtils.equalObjects(modelProject.getId(), project.getId())) { + return modelProject; + } } } return null; @@ -141,4 +140,10 @@ public DBPProject getOwnerProject() { public DBPObject getObjectDetails(@NotNull DBRProgressMonitor monitor, @NotNull SMSessionContext sessionContext, @NotNull Object dataSource) throws DBException { return project; } + + @Override + public String toString() { + return getNodeDisplayName(); + } + } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java index 1ed91657a7..4e2cfbc76f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java @@ -1,18 +1,18 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp + * Copyright (C) 2010-2024 DBeaver Corp and others * - * All Rights Reserved. + * 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 * - * 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. + * 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.model.rm; @@ -55,7 +55,7 @@ public String getNodeType() { } @Override - public String getNodeName() { + public String getNodeDisplayName() { return resource.getName(); } @@ -80,7 +80,7 @@ public DBPImage getNodeIcon() { } return DBIcon.TREE_FOLDER; } else { - var fileExtension = IOUtils.getFileExtension(getNodeName()); + var fileExtension = IOUtils.getFileExtension(getNodeDisplayName()); if (!CommonUtils.isEmpty(fileExtension)) { RMProject project = getProjectNode(); if (project != null) { @@ -110,8 +110,8 @@ protected boolean allowsChildren() { } @Override - public DBNNode[] getChildren(DBRProgressMonitor monitor) throws DBException { - if (children == null) { + public DBNNode[] getChildren(@NotNull DBRProgressMonitor monitor) throws DBException { + if (children == null && !monitor.isForceCacheUsage()) { List rfList = new ArrayList<>(); for (RMResource resource : getResourceController().listResources( getResourceProject().getId(), getResourceFolder(), null, true, false, false)) { @@ -154,9 +154,10 @@ protected RMController getResourceController() throws DBException { throw new DBException("Can't detect resource root node"); } + @Deprecated @Override public String getNodeItemPath() { - return getParentNode().getNodeItemPath() + "/" + getNodeName(); + return getParentNode().getNodeItemPath() + "/" + getNodeDisplayName(); } @Override @@ -174,7 +175,7 @@ public void rename(DBRProgressMonitor monitor, String newName) throws DBExceptio String resourceName = resource.getName(); try { if (newName.indexOf('.') == -1) { - String ext = IOUtils.getFileExtension(getNodeName()); + String ext = IOUtils.getFileExtension(getNodeDisplayName()); if (!CommonUtils.isEmpty(ext)) { newName += "." + ext; } @@ -193,7 +194,7 @@ public void rename(DBRProgressMonitor monitor, String newName) throws DBExceptio @Override public String toString() { - return getNodeName(); + return getNodeDisplayName(); } @Nullable @@ -202,9 +203,10 @@ public DBPObject getObjectDetails(@NotNull DBRProgressMonitor monitor, @NotNull return resource; } + @Nullable @Override - public DBPProject getOwnerProject() { - return getParentNode().getOwnerProject(); + public DBPProject getOwnerProjectOrNull() { + return getParentNode().getOwnerProjectOrNull(); } public RMResource getResource() { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerRoot.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerRoot.java index a92a3997f2..d03d823cdb 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerRoot.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerRoot.java @@ -1,24 +1,25 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp + * Copyright (C) 2010-2024 DBeaver Corp and others * - * All Rights Reserved. + * 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 * - * 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. + * 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.model.rm; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPHiddenObject; @@ -54,8 +55,14 @@ public String getNodeType() { return "rm"; } + @NotNull @Override - public String getNodeName() { + public String getNodeId() { + return "rm"; + } + + @Override + public String getNodeDisplayName() { return "resources"; } @@ -75,8 +82,8 @@ protected boolean allowsChildren() { } @Override - public DBNResourceManagerProject[] getChildren(DBRProgressMonitor monitor) throws DBException { - if (projects == null) { + public DBNResourceManagerProject[] getChildren(@NotNull DBRProgressMonitor monitor) throws DBException { + if (projects == null && !monitor.isForceCacheUsage()) { List projectList = getParentNode().getModel().getModelProjects(); if (CommonUtils.isEmpty(projectList)) { return new DBNResourceManagerProject[0]; @@ -103,10 +110,11 @@ public DBNResourceManagerProject[] getChildren(DBRProgressMonitor monitor) throw return projects; } + @Deprecated @Override public String getNodeItemPath() { // Path doesn't include project name - return NodePathType.ext.getPrefix() + getNodeName(); + return NodePathType.ext.getPrefix() + getNodeDisplayName(); } @Override @@ -115,11 +123,6 @@ public DBNNode refreshNode(DBRProgressMonitor monitor, Object source) throws DBE return this; } - @Override - public String toString() { - return getNodeName(); - } - @Override public boolean isHidden() { return true; @@ -152,8 +155,13 @@ private void deleteResourceNode(RMProject project, String resourcePath) { return; } var projectNode = getProjectNode(project); - var rmResourcePath = Arrays.asList(resourcePath.split("/")); - projectNode.ifPresent(dbnResourceManagerProject -> dbnResourceManagerProject.removeChildResourceNode(new ArrayDeque<>(rmResourcePath))); + var rmResourcePath = Arrays.stream(resourcePath.split("/")) + .filter(CommonUtils::isNotEmpty) + .toList(); + projectNode.ifPresent( + dbnResourceManagerProject -> dbnResourceManagerProject.removeChildResourceNode(new ArrayDeque<>( + rmResourcePath)) + ); } @NotNull diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/RMControllerInvocationHandler.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/RMControllerInvocationHandler.java deleted file mode 100644 index 74eae69dc4..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/RMControllerInvocationHandler.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.model.rm; - -import io.cloudbeaver.model.app.WebApplication; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.rm.RMController; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -public class RMControllerInvocationHandler implements InvocationHandler { - private final WebApplication webApplication; - private final RMController rmController; - - public RMControllerInvocationHandler(RMController rmController, WebApplication webApplication) { - this.webApplication = webApplication; - this.rmController = rmController; - } - - private void checkIsRmEnabled() throws DBException { - if (!webApplication.getAppConfiguration().isResourceManagerEnabled()) { - throw new DBException("Resource Manager disabled"); - } - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - try { - checkIsRmEnabled(); - return method.invoke(rmController, args); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - } -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/BaseLocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/BaseLocalResourceController.java new file mode 100644 index 0000000000..0e18ed7f0c --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/BaseLocalResourceController.java @@ -0,0 +1,365 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model.rm.local; + +import io.cloudbeaver.BaseWebProjectImpl; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceConfigurationStorage; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.DBPDataSourceFolder; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.dbeaver.model.fs.lock.FileLockController; +import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.rm.RMEvent; +import org.jkiss.dbeaver.model.rm.RMEventManager; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; +import org.jkiss.dbeaver.registry.*; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.utils.ArrayUtils; +import org.jkiss.utils.IOUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.function.Predicate; + +public abstract class BaseLocalResourceController implements RMController { + private static final Log log = Log.getLog(BaseLocalResourceController.class); + + public static final String DEFAULT_CHANGE_ID = "0"; + private static final String FILE_REGEX = "(?U)[\\w.$()@/\\\\ -]+"; + private static final String PROJECT_REGEX = "(?U)[\\w.$()@ -]+"; // slash not allowed in project name + + @NotNull + protected final DBPWorkspace workspace; + @NotNull + protected final FileLockController lockController; + + protected BaseLocalResourceController( + @NotNull DBPWorkspace workspace, + @NotNull FileLockController lockController + ) { + this.workspace = workspace; + this.lockController = lockController; + } + + @Override + public RMProject getProject(@NotNull String projectId, boolean readResources, boolean readProperties) + throws DBException { + RMProject project = makeProjectFromId(projectId, true); + if (project == null) { + return null; + } + if (readResources) { + doProjectOperation(projectId, () -> { + project.setChildren( + listResources(projectId, null, null, readProperties, false, true) + ); + return null; + }); + } + return project; + } + + @Override + public Object getProjectProperty(@NotNull String projectId, @NotNull String propName) throws DBException { + var project = getWebProject(projectId, false); + return doFileReadOperation(projectId, + project.getMetadataFilePath(), + () -> project.getProjectProperty(propName)); + } + + @Override + public void setProjectProperty( + @NotNull String projectId, + @NotNull String propName, + @NotNull Object propValue + ) throws DBException { + BaseWebProjectImpl webProject = getWebProject(projectId, false); + doFileWriteOperation(projectId, webProject.getMetadataFilePath(), + () -> { + log.debug("Updating value for property '" + propName + "' in project '" + projectId + "'"); + webProject.setProjectProperty(propName, propValue); + return null; + } + ); + } + + @Override + public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds) + throws DBException { + DBPProject projectMetadata = getWebProject(projectId, false); + return doFileReadOperation( + projectId, + projectMetadata.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = projectMetadata.getDataSourceRegistry(); + registry.refreshConfig(); + registry.checkForErrors(); + DataSourceConfigurationManagerBuffer buffer = new DataSourceConfigurationManagerBuffer(); + Predicate filter = null; + if (!ArrayUtils.isEmpty(dataSourceIds)) { + filter = ds -> ArrayUtils.contains(dataSourceIds, ds.getId()); + } + ((DataSourcePersistentRegistry) registry).saveConfigurationToManager(new VoidProgressMonitor(), + buffer, + filter); + registry.checkForErrors(); + + return new String(buffer.getData(), StandardCharsets.UTF_8); + } + ); + } + + @Override + public void createProjectDataSources( + @NotNull String projectId, + @NotNull String configuration, + @Nullable List dataSourceIds + ) throws DBException { + updateProjectDataSources(projectId, configuration, dataSourceIds); + } + + @Override + public boolean updateProjectDataSources( + @NotNull String projectId, + @NotNull String configuration, + @Nullable List dataSourceIds + ) throws DBException { + return updateProjectDataSourcesConfig(projectId, configuration, dataSourceIds) != null; + } + + @Nullable + protected DataSourceParseResults updateProjectDataSourcesConfig( + @NotNull String projectId, + @NotNull String configuration, + @Nullable List dataSourceIds + ) throws DBException { + try (var lock = lockController.lock(projectId, "updateProjectDataSources")) { + DBPProject project = getWebProject(projectId, false); + return doFileWriteOperation( + projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + DBPDataSourceConfigurationStorage storage = new DataSourceMemoryStorage(configuration.getBytes( + StandardCharsets.UTF_8)); + DataSourceConfigurationManager manager = new DataSourceConfigurationManagerBuffer(); + final DataSourceParseResults parseResults = ((DataSourcePersistentRegistry) registry).loadDataSources( + List.of(storage), + manager, + dataSourceIds, + true, + dataSourceIds == null + ); + registry.checkForErrors(); + log.debug("Save data sources configuration in project '" + projectId + "'"); + ((DataSourcePersistentRegistry) registry).saveDataSources(); + registry.checkForErrors(); + return parseResults; + } + ); + } + } + + @Override + public void deleteProjectDataSources( + @NotNull String projectId, + @NotNull String[] dataSourceIds + ) throws DBException { + try (var projectLock = lockController.lock(projectId, "deleteDatasources")) { + DBPProject project = getWebProject(projectId, false); + doFileWriteOperation(projectId, project.getMetadataFolder(false), () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + for (String dataSourceId : dataSourceIds) { + DBPDataSourceContainer dataSource = registry.getDataSource(dataSourceId); + + if (dataSource != null) { + log.debug("Deleting data source '" + dataSourceId + "' in project '" + projectId + "'"); + registry.removeDataSource(dataSource); + } else { + log.warn("Could not find datasource " + dataSourceId + " for deletion"); + } + } + registry.checkForErrors(); + return null; + }); + } + } + + @Override + public void createProjectDataSourceFolder( + @NotNull String projectId, + @NotNull String folderPath + ) throws DBException { + try (var projectLock = lockController.lock(projectId, "createDatasourceFolder")) { + DBPProject project = getWebProject(projectId, false); + log.debug("Creating data source folder '" + folderPath + "' in project '" + projectId + "'"); + doFileWriteOperation(projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + var result = Path.of(folderPath); + var newName = result.getFileName().toString(); + GeneralUtils.validateResourceName(newName); + var parent = result.getParent(); + var parentFolder = parent == null ? null : registry.getFolder(parent.toString().replace("\\", "/")); + DBPDataSourceFolder newFolder = registry.addFolder(parentFolder, newName); + ((DataSourcePersistentRegistry) registry).saveDataSources(); + registry.checkForErrors(); + return null; + } + ); + } + } + + @Override + public void deleteProjectDataSourceFolders( + @NotNull String projectId, + @NotNull String[] folderPaths, + boolean dropContents + ) throws DBException { + try (var projectLock = lockController.lock(projectId, "createDatasourceFolder")) { + DBPProject project = getWebProject(projectId, false); + doFileWriteOperation(projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + for (String folderPath : folderPaths) { + DBPDataSourceFolder folder = registry.getFolder(folderPath); + if (folder != null) { + log.debug("Deleting data source folder '" + folderPath + "' in project '" + projectId + "'"); + registry.removeFolder(folder, dropContents); + } else { + log.warn("Can not find folder by path [" + folderPath + "] for deletion"); + } + } + ((DataSourcePersistentRegistry) registry).saveDataSources(); + registry.checkForErrors(); + return null; + } + ); + } + } + + @Override + public void moveProjectDataSourceFolder( + @NotNull String projectId, + @NotNull String oldPath, + @NotNull String newPath + ) throws DBException { + try (var projectLock = lockController.lock(projectId, "createDatasourceFolder")) { + DBPProject project = getWebProject(projectId, false); + log.debug("Moving data source folder from '" + oldPath + "' to '" + newPath + "' in project '" + projectId + "'"); + doFileWriteOperation(projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + registry.moveFolder(oldPath, newPath); + registry.checkForErrors(); + ((DataSourcePersistentRegistry) registry).saveDataSources(); + return null; + } + ); + } + } + + protected abstract BaseWebProjectImpl getWebProject(String projectId, boolean refresh) throws DBException; + + protected abstract T doFileWriteOperation(String projectId, Path file, RMFileOperation operation) + throws DBException; + + protected abstract T doFileReadOperation(String projectId, Path file, RMFileOperation operation) + throws DBException; + + protected abstract T doProjectOperation(String projectId, RMFileOperation operation) throws DBException; + + protected abstract RMProject makeProjectFromId(String projectId, boolean loadPermissions) throws DBException; + + protected void validateResourcePath(String resourcePath) throws DBException { + var fullPath = Paths.get(resourcePath); + for (Path path : fullPath) { + String fileName = IOUtils.getFileNameWithoutExtension(path); + GeneralUtils.validateResourceName(fileName); + } + } + + protected void createFolder(Path targetPath) throws DBException { + if (!Files.exists(targetPath)) { + try { + Files.createDirectories(targetPath); + } catch (IOException e) { + throw new DBException("Error creating folder '" + targetPath + "'"); + } + } + } + + protected class InternalWebProjectImpl extends BaseWebProjectImpl { + public InternalWebProjectImpl( + @NotNull SessionContextImpl sessionContext, + @NotNull RMProject rmProject, + @NotNull Path projectPath + ) { + super( + BaseLocalResourceController.this.workspace, + BaseLocalResourceController.this, + sessionContext, + rmProject, + projectPath + ); + } + + @NotNull + @Override + protected DBPDataSourceRegistry createDataSourceRegistry() { + return new DataSourceRegistry(this); + } + } + + protected void fireRmResourceAddEvent(@NotNull String projectId, @NotNull String resourcePath) throws DBException { + RMEventManager.fireEvent( + new RMEvent(RMEvent.Action.RESOURCE_ADD, + getProject(projectId, false, false), + resourcePath) + ); + } + + protected void fireRmResourceDeleteEvent(@NotNull String projectId, @NotNull String resourcePath) + throws DBException { + RMEventManager.fireEvent( + new RMEvent(RMEvent.Action.RESOURCE_DELETE, + makeProjectFromId(projectId, false), + resourcePath + ) + ); + } + + protected void fireRmProjectAddEvent(@NotNull RMProject project) { + RMEventManager.fireEvent( + new RMEvent( + RMEvent.Action.RESOURCE_ADD, + project + ) + ); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index a12d43a2bc..8138830b8f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -18,48 +18,49 @@ import io.cloudbeaver.BaseWebProjectImpl; import io.cloudbeaver.DBWConstants; -import io.cloudbeaver.model.app.WebApplication; -import io.cloudbeaver.model.rm.lock.RMFileLockController; +import io.cloudbeaver.model.app.ServletApplication; import io.cloudbeaver.service.security.SMUtils; import io.cloudbeaver.service.sql.WebSQLConstants; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.utils.ServletAppUtils; import io.cloudbeaver.utils.file.UniversalFileVisitor; import org.eclipse.core.runtime.IPath; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.DBPDataSourceConfigurationStorage; import org.jkiss.dbeaver.model.DBPDataSourceContainer; -import org.jkiss.dbeaver.model.DBPDataSourceFolder; import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; -import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMCredentials; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +import org.jkiss.dbeaver.model.fs.lock.FileLockController; +import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl; import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl; +import org.jkiss.dbeaver.model.navigator.DBNLocalFolder; import org.jkiss.dbeaver.model.rm.*; -import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; import org.jkiss.dbeaver.model.security.SMController; import org.jkiss.dbeaver.model.security.SMObjectType; import org.jkiss.dbeaver.model.sql.DBQuotaException; import org.jkiss.dbeaver.model.websocket.event.MessageType; -import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.WSSessionLogUpdatedEvent; -import org.jkiss.dbeaver.registry.*; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDatasourceFolderEvent; +import org.jkiss.dbeaver.registry.DataSourceDescriptor; +import org.jkiss.dbeaver.registry.DataSourceParseResults; +import org.jkiss.dbeaver.registry.ResourceTypeDescriptor; +import org.jkiss.dbeaver.registry.ResourceTypeRegistry; import org.jkiss.dbeaver.runtime.DBWorkbench; -import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; import org.jkiss.utils.Pair; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.*; +import java.text.MessageFormat; import java.time.OffsetDateTime; import java.time.ZoneId; import java.util.*; -import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -67,15 +68,10 @@ /** * Resource manager API */ -public class LocalResourceController implements RMController { +public class LocalResourceController extends BaseLocalResourceController { private static final Log log = Log.getLog(LocalResourceController.class); - private static final String FILE_REGEX = "(?U)[\\w.$()@/\\\\ -]+"; - private static final String PROJECT_REGEX = "(?U)[\\w.$()@ -]+"; // slash not allowed in project name - public static final String DEFAULT_CHANGE_ID = "0"; - - private final DBPWorkspace workspace; protected final SMCredentialsProvider credentialsProvider; private final Path rootPath; @@ -83,7 +79,6 @@ public class LocalResourceController implements RMController { private final Path sharedProjectsPath; private final String globalProjectName; private Supplier smControllerSupplier; - protected final RMFileLockController lockController; protected final List fileHandlers; private final Map projectRegistries = new LinkedHashMap<>(); @@ -96,13 +91,12 @@ public LocalResourceController( Path sharedProjectsPath, Supplier smControllerSupplier ) throws DBException { - this.workspace = workspace; + super(workspace, new FileLockController(ServletAppUtils.getServletApplication().getApplicationInstanceId())); this.credentialsProvider = credentialsProvider; this.rootPath = rootPath; this.userProjectsPath = userProjectsPath; this.sharedProjectsPath = sharedProjectsPath; this.smControllerSupplier = smControllerSupplier; - this.lockController = new RMFileLockController(WebAppUtils.getWebApplication()); this.globalProjectName = DBWorkbench.getPlatform().getApplication().getDefaultProjectName(); this.fileHandlers = RMFileOperationHandlersRegistry.getInstance().getFileHandlers(); @@ -128,18 +122,22 @@ protected BaseWebProjectImpl getWebProject(String projectId, boolean refresh) th if (project == null || refresh) { SessionContextImpl sessionContext = new SessionContextImpl(null); RMProject rmProject = makeProjectFromId(projectId, false); - project = new BaseWebProjectImpl( - workspace, - this, - sessionContext, - rmProject, - (container) -> true); + project = createWebProjectImpl(projectId, sessionContext, rmProject); projectRegistries.put(projectId, project); } return project; } } + @NotNull + protected InternalWebProjectImpl createWebProjectImpl( + String projectId, + SessionContextImpl sessionContext, + RMProject rmProject + ) throws DBException { + return new InternalWebProjectImpl(sessionContext, rmProject, getProjectPath(projectId)); + } + @NotNull @Override public RMProject[] listAccessibleProjects() throws DBException { @@ -163,7 +161,7 @@ public RMProject[] listAccessibleProjects() throws DBException { } // Checking if private projects are enabled in the configuration and if the user has permission to them - var webApp = WebAppUtils.getWebApplication(); + var webApp = ServletAppUtils.getServletApplication(); var userHasPrivateProjectPermission = userHasAccessToPrivateProject(webApp, activeUserCreds); if (webApp.getAppConfiguration().isSupportsCustomConnections() && userHasPrivateProjectPermission) { var userProjectPermission = getProjectPermissions(null, RMProjectType.USER); @@ -172,7 +170,7 @@ public RMProject[] listAccessibleProjects() throws DBException { projects.add(0, userProject); } } - if (WebAppUtils.getWebApplication().isMultiNode()) { + if (ServletAppUtils.getServletApplication().isMultiNode()) { for (RMProject rmProject : projects) { handleProjectOpened(rmProject.getId()); } @@ -218,7 +216,7 @@ private Set getProjectPermissions(@Nullable String projectI } return getRmProjectPermissions(projectId, activeUserCreds); case USER: - var webApp = WebAppUtils.getWebApplication(); + var webApp = ServletAppUtils.getServletApplication(); if (userHasAccessToPrivateProject(webApp, activeUserCreds)) { return Set.of(RMProjectPermission.RESOURCE_EDIT, RMProjectPermission.DATA_SOURCES_EDIT); } @@ -227,7 +225,7 @@ private Set getProjectPermissions(@Nullable String projectI } } - private boolean userHasAccessToPrivateProject(WebApplication webApp, @Nullable SMCredentials activeUserCreds) { + private boolean userHasAccessToPrivateProject(ServletApplication webApp, @Nullable SMCredentials activeUserCreds) { return !webApp.isMultiNode() || (activeUserCreds != null && activeUserCreds.hasPermission(DBWConstants.PERMISSION_PRIVATE_PROJECT_ACCESS)); } @@ -254,18 +252,20 @@ public RMProject[] listAllSharedProjects() throws DBException { return new RMProject[0]; } var projects = new ArrayList(); - var allPaths = Files.list(sharedProjectsPath).collect(Collectors.toList()); - for (Path path : allPaths) { - var projectPerms = getProjectPermissions( - makeProjectIdFromPath(path, RMProjectType.SHARED), - RMProjectType.SHARED - ); - var rmProject = makeProjectFromPath(path, projectPerms, RMProjectType.SHARED, false); - projects.add(rmProject); + try (Stream list = Files.list(sharedProjectsPath)) { + var allPaths = list.toList(); + for (Path path : allPaths) { + var projectPerms = getProjectPermissions( + makeProjectIdFromPath(path, RMProjectType.SHARED), + RMProjectType.SHARED + ); + var rmProject = makeProjectFromPath(path, projectPerms, RMProjectType.SHARED, false); + projects.add(rmProject); + } + return projects.stream() + .filter(Objects::nonNull) + .toArray(RMProject[]::new); } - return projects.stream() - .filter(Objects::nonNull) - .toArray(RMProject[]::new); } catch (IOException e) { throw new DBException("Error reading shared projects", e); } @@ -280,7 +280,7 @@ public RMProject createProject(@NotNull String name, @Nullable String descriptio throw new DBException("Error creating shared project path", e); } } - validateResourcePath(name, PROJECT_REGEX); + validateResourcePath(name); RMProject project; var projectPath = sharedProjectsPath.resolve(name); if (Files.exists(projectPath)) { @@ -293,7 +293,7 @@ public RMProject createProject(@NotNull String name, @Nullable String descriptio try { log.debug("Creating project '" + project.getId() + "'"); Files.createDirectories(projectPath); - if (WebAppUtils.getWebApplication().isMultiNode()) { + if (ServletAppUtils.getServletApplication().isMultiNode()) { createResourceTypeFolders(projectPath); } fireRmProjectAddEvent(project); @@ -305,11 +305,12 @@ public RMProject createProject(@NotNull String name, @Nullable String descriptio @Override public void deleteProject(@NotNull String projectId) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "deleteProject")) { + try (var projectLock = lockController.lock(projectId, "deleteProject")) { RMProject project = makeProjectFromId(projectId, false); Path targetPath = getProjectPath(projectId); if (!Files.exists(targetPath)) { - throw new DBException("Project '" + project.getName() + "' doesn't exists"); + log.debug(MessageFormat.format("Project folder ''{0}'' is not found", projectId)); + return; } try { log.debug("Deleting project '" + projectId + "'"); @@ -341,58 +342,24 @@ public RMProject getProject(@NotNull String projectId, boolean readResources, bo return project; } - @Override - public Object getProjectProperty(@NotNull String projectId, @NotNull String propName) throws DBException { - var project = getWebProject(projectId, false); - return doFileReadOperation(projectId, project.getMetadataFilePath(), () -> project.getProjectProperty(propName)); - } - - @Override - public void setProjectProperty( - @NotNull String projectId, - @NotNull String propName, - @NotNull Object propValue - ) throws DBException { - BaseWebProjectImpl webProject = getWebProject(projectId, false); - doFileWriteOperation(projectId, webProject.getMetadataFilePath(), - () -> { - log.debug("Updating value for property '" + propName + "' in project '" + projectId + "'"); - webProject.setProjectProperty(propName, propValue); - return null; - } - ); - } - - @Override - public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds) throws DBException { - DBPProject projectMetadata = getWebProject(projectId, false); - return doFileReadOperation( - projectId, - projectMetadata.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = projectMetadata.getDataSourceRegistry(); - registry.refreshConfig(); - registry.checkForErrors(); - DataSourceConfigurationManagerBuffer buffer = new DataSourceConfigurationManagerBuffer(); - Predicate filter = null; - if (!ArrayUtils.isEmpty(dataSourceIds)) { - filter = ds -> ArrayUtils.contains(dataSourceIds, ds.getId()); - } - ((DataSourcePersistentRegistry) registry).saveConfigurationToManager(new VoidProgressMonitor(), buffer, filter); - registry.checkForErrors(); - - return new String(buffer.getData(), StandardCharsets.UTF_8); - } - ); - } - @Override public void createProjectDataSources( @NotNull String projectId, @NotNull String configuration, @Nullable List dataSourceIds ) throws DBException { - updateProjectDataSources(projectId, configuration, dataSourceIds); + super.createProjectDataSources(projectId, configuration, dataSourceIds); + if (credentialsProvider.getActiveUserCredentials() != null && dataSourceIds != null) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDataSourceEvent.create( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + projectId, + dataSourceIds, + WSDataSourceProperty.CONFIGURATION + ) + ); + } } @Override @@ -401,119 +368,211 @@ public boolean updateProjectDataSources( @NotNull String configuration, @Nullable List dataSourceIds ) throws DBException { - try (var lock = lockController.lockProject(projectId, "updateProjectDataSources")) { - DBPProject project = getWebProject(projectId, false); - return doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - DBPDataSourceConfigurationStorage storage = new DataSourceMemoryStorage(configuration.getBytes(StandardCharsets.UTF_8)); - DataSourceConfigurationManager manager = new DataSourceConfigurationManagerBuffer(); - var configChanged = ((DataSourcePersistentRegistry) registry).loadDataSources( - List.of(storage), - manager, - dataSourceIds, - true, - false - ); - registry.checkForErrors(); - log.debug("Save data sources configuration in project '" + projectId + "'"); - ((DataSourcePersistentRegistry) registry).saveDataSources(); - registry.checkForErrors(); - return configChanged; - } + DBPDataSourceRegistry registry = getWebProject(projectId, false).getDataSourceRegistry(); + Map oldDataSources = registry.getDataSources().stream() + .filter(ds -> dataSourceIds == null || dataSourceIds.contains(ds.getId())) + .collect(Collectors.toMap( + DBPDataSourceContainer::getId, + registry::createDataSource + ) ); - } + DataSourceParseResults parseResults = super.updateProjectDataSourcesConfig(projectId, configuration, dataSourceIds); + sendDataSourcesConfigUpdatedEvent(registry, oldDataSources, parseResults); + return parseResults != null; } @Override - public void deleteProjectDataSources(@NotNull String projectId, - @NotNull String[] dataSourceIds) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "deleteDatasources")) { - DBPProject project = getWebProject(projectId, false); - doFileWriteOperation(projectId, project.getMetadataFolder(false), () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - for (String dataSourceId : dataSourceIds) { - DBPDataSourceContainer dataSource = registry.getDataSource(dataSourceId); - - if (dataSource != null) { - log.debug("Deleting data source '" + dataSourceId + "' in project '" + projectId + "'"); - registry.removeDataSource(dataSource); - } else { - log.warn("Could not find datasource " + dataSourceId + " for deletion"); - } - } - registry.checkForErrors(); - return null; - }); + public void deleteProjectDataSources(@NotNull String projectId, @NotNull String[] dataSourceIds) throws DBException { + super.deleteProjectDataSources(projectId, dataSourceIds); + if (credentialsProvider.getActiveUserCredentials() != null && dataSourceIds.length > 0) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDataSourceEvent.delete( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + projectId, + Arrays.asList(dataSourceIds), + WSDataSourceProperty.CONFIGURATION + ) + ); } } @Override - public void createProjectDataSourceFolder(@NotNull String projectId, - @NotNull String folderPath) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { - DBPProject project = getWebProject(projectId, false); - log.debug("Creating data source folder '" + folderPath + "' in project '" + projectId + "'"); - doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - var result = Path.of(folderPath); - var newName = result.getFileName().toString(); - var parent = result.getParent(); - var parentFolder = parent == null ? null : registry.getFolder(parent.toString().replace("\\", "/")); - DBPDataSourceFolder newFolder = registry.addFolder(parentFolder, newName); - registry.checkForErrors(); - return null; - } + public void createProjectDataSourceFolder(@NotNull String projectId, @NotNull String folderPath) throws DBException { + super.createProjectDataSourceFolder(projectId, folderPath); + if (credentialsProvider.getActiveUserCredentials() != null) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDatasourceFolderEvent.create( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + projectId, + List.of(createNodePathFromFolderPath(projectId, folderPath)) + ) ); } } @Override - public void deleteProjectDataSourceFolders( + public void moveProjectDataSourceFolder( @NotNull String projectId, - @NotNull String[] folderPaths, - boolean dropContents + @NotNull String oldPath, + @NotNull String newPath ) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { - DBPProject project = getWebProject(projectId, false); - doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - for (String folderPath : folderPaths) { - DBPDataSourceFolder folder = registry.getFolder(folderPath); - if (folder != null) { - log.debug("Deleting data source folder '" + folderPath + "' in project '" + projectId + "'"); - registry.removeFolder(folder, dropContents); - } else { - log.warn("Can not find folder by path [" + folderPath + "] for deletion"); - } - } - registry.checkForErrors(); - return null; - } + super.moveProjectDataSourceFolder(projectId, oldPath, newPath); + if (credentialsProvider.getActiveUserCredentials() != null) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDatasourceFolderEvent.delete( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + projectId, + List.of(createNodePathFromFolderPath(projectId, oldPath)) + ) + ); + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDatasourceFolderEvent.create( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + projectId, + List.of(createNodePathFromFolderPath(projectId, newPath)) + ) ); } } @Override - public void moveProjectDataSourceFolder( - @NotNull String projectId, - @NotNull String oldPath, - @NotNull String newPath - ) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { - DBPProject project = getWebProject(projectId, false); - log.debug("Moving data source folder from '" + oldPath + "' to '" + newPath + "' in project '" + projectId + "'"); - doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - registry.moveFolder(oldPath, newPath); - registry.checkForErrors(); - return null; - } + public void deleteProjectDataSourceFolders(@NotNull String projectId, @NotNull String[] folderPaths, boolean dropContents) + throws DBException { + super.deleteProjectDataSourceFolders(projectId, folderPaths, dropContents); + if (credentialsProvider.getActiveUserCredentials() != null) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDatasourceFolderEvent.create( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + projectId, + Arrays.stream(folderPaths).map( + p -> createNodePathFromFolderPath(projectId, p) + ).collect(Collectors.toList()) + ) + ); + } + } + + private String createNodePathFromFolderPath(String projectId, String folderPath) { + return DBNLocalFolder.makeLocalFolderItemPath(projectId, folderPath); + } + + private void sendDataSourcesConfigUpdatedEvent( + @NotNull DBPDataSourceRegistry registry, + @NotNull Map oldDataSources, + @Nullable DataSourceParseResults parseResults + ) { + if (parseResults == null || credentialsProvider.getActiveUserCredentials() == null || oldDataSources.isEmpty()) { + return; + } + List updatedConfigurationDataSourceIds = new ArrayList<>(); + List updatedNameDataSourceIds = new ArrayList<>(); + List updatedInternalConfigurationDataSourceIds = new ArrayList<>(); + + for (Map.Entry entry : oldDataSources.entrySet()) { + String dsId = entry.getKey(); + DataSourceDescriptor oldDs = entry.getValue(); + DataSourceDescriptor newDs = (DataSourceDescriptor) registry.getDataSource(dsId); + if (newDs == null) { + continue; + } + if (!oldDs.equalConfiguration(newDs)) { + updatedConfigurationDataSourceIds.add(dsId); + } else if (!oldDs.isLooselyEqualTo(newDs)) { + updatedNameDataSourceIds.add(dsId); + } else if (!oldDs.equalInternalConfiguration(newDs)) { + updatedInternalConfigurationDataSourceIds.add(dsId); + } + } + + if (!updatedConfigurationDataSourceIds.isEmpty()) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDataSourceEvent.update( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + registry.getProject().getId(), + updatedConfigurationDataSourceIds, + WSDataSourceProperty.CONFIGURATION + ) + ); + } + if (!updatedNameDataSourceIds.isEmpty()) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDataSourceEvent.update( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + registry.getProject().getId(), + updatedNameDataSourceIds, + WSDataSourceProperty.NAME + ) + ); + } + if (!updatedInternalConfigurationDataSourceIds.isEmpty()) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDataSourceEvent.update( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + registry.getProject().getId(), + updatedInternalConfigurationDataSourceIds, + WSDataSourceProperty.INTERNAL + ) + ); + } + + if (!parseResults.addedDataSources.isEmpty()) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDataSourceEvent.create( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + registry.getProject().getId(), + updatedNameDataSourceIds, + WSDataSourceProperty.CONFIGURATION + ) + ); + } + + if (!parseResults.removedDataSources.isEmpty()) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDataSourceEvent.delete( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + registry.getProject().getId(), + updatedNameDataSourceIds, + WSDataSourceProperty.CONFIGURATION + ) + ); + } + + if (!parseResults.addedFolders.isEmpty()) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDatasourceFolderEvent.create( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + registry.getProject().getId(), + parseResults.addedFolders.stream().map( + f -> createNodePathFromFolderPath(registry.getProject().getId(), f.getFolderPath()) + ).toList() + ) + ); + } + + if (!parseResults.removedFolders.isEmpty()) { + ServletAppUtils.getServletApplication().getEventController().addEvent( + WSDatasourceFolderEvent.delete( + credentialsProvider.getActiveUserCredentials().getSmSessionId(), + credentialsProvider.getActiveUserCredentials().getUserId(), + registry.getProject().getId(), + parseResults.removedFolders.stream().map( + f -> createNodePathFromFolderPath(registry.getProject().getId(), f.getFolderPath()) + ).toList() + ) ); } + } @NotNull @@ -577,7 +636,7 @@ public String createResource( @NotNull String resourcePath, boolean isFolder ) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "createResource")) { + try (var ignoredLock = lockController.lock(projectId, "createResource")) { validateResourcePath(resourcePath); Path targetPath = getTargetPath(projectId, resourcePath); if (Files.exists(targetPath)) { @@ -609,7 +668,7 @@ public String moveResource( @NotNull String oldResourcePath, @NotNull String newResourcePath ) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "moveResource")) { + try (var ignoredLock = lockController.lock(projectId, "moveResource")) { var normalizedOldResourcePath = CommonUtils.normalizeResourcePath(oldResourcePath); var normalizedNewResourcePath = CommonUtils.normalizeResourcePath(newResourcePath); if (log.isDebugEnabled()) { @@ -623,7 +682,13 @@ public String moveResource( throw new DBException("Resource '" + oldTargetPath + "' doesn't exists"); } Path newTargetPath = getTargetPath(projectId, normalizedNewResourcePath); - validateResourcePath(newTargetPath.toString()); + validateResourcePath(rootPath.relativize(newTargetPath).toString()); + if (Files.exists(newTargetPath)) { + throw new DBException("Resource with name %s already exists".formatted(newTargetPath.getFileName())); + } + if (!Files.exists(newTargetPath.getParent())) { + throw new DBException("Resource %s doesn't exists".formatted(newTargetPath.getParent().getFileName())); + } try { Files.move(oldTargetPath, newTargetPath); } catch (IOException e) { @@ -675,11 +740,10 @@ private void movePropertiesRecursive( @Override public void deleteResource(@NotNull String projectId, @NotNull String resourcePath, boolean recursive) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "deleteResource")) { + try (var ignoredLock = lockController.lock(projectId, "deleteResource")) { if (log.isDebugEnabled()) { log.debug("Removing resource from '" + resourcePath + "' in project '" + projectId + "'" + (recursive ? " recursive" : "")); } - validateResourcePath(resourcePath); Path targetPath = getTargetPath(projectId, resourcePath); doFileWriteOperation(projectId, targetPath, () -> { if (!Files.exists(targetPath)) { @@ -696,7 +760,7 @@ public void deleteResource(@NotNull String projectId, @NotNull String resourcePa log.warn("Failed to remove resources properties", e); } try { - if (targetPath.toFile().isDirectory()) { + if (Files.isDirectory(targetPath)) { IOUtils.deleteDirectory(targetPath); } else { Files.delete(targetPath); @@ -764,9 +828,9 @@ public String setResourceContents( @NotNull byte[] data, boolean forceOverwrite ) throws DBException { - try (var lock = lockController.lockProject(projectId, "setResourceContents")) { + try (var ignoredLock = lockController.lock(projectId, "setResourceContents")) { validateResourcePath(resourcePath); - Number fileSizeLimit = WebAppUtils.getWebApplication() + Number fileSizeLimit = ServletAppUtils.getServletApplication() .getAppConfiguration() .getResourceQuota(WebSQLConstants.QUOTA_PROP_RM_FILE_SIZE_LIMIT); if (fileSizeLimit != null && data.length > fileSizeLimit.longValue()) { @@ -800,15 +864,6 @@ public String setResourceContents( return DEFAULT_CHANGE_ID; } - protected void createFolder(Path targetPath) throws DBException { - if (!Files.exists(targetPath)) { - try { - Files.createDirectories(targetPath); - } catch (IOException e) { - throw new DBException("Error creating folder '" + targetPath + "'"); - } - } - } @NotNull @Override @@ -818,7 +873,7 @@ public String setResourceProperty( @NotNull String propertyName, @Nullable Object propertyValue ) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "resourcePropertyUpdate")) { + try (var ignoredLock = lockController.lock(projectId, "resourcePropertyUpdate")) { validateResourcePath(resourcePath); BaseWebProjectImpl webProject = getWebProject(projectId, false); doFileWriteOperation(projectId, webProject.getMetadataFilePath(), @@ -832,20 +887,24 @@ public String setResourceProperty( } } - private void validateResourcePath(String resourcePath) throws DBException { - validateResourcePath(resourcePath, FILE_REGEX); - } - - private void validateResourcePath(String resourcePath, String regex) throws DBException { - var fullPath = Paths.get(resourcePath); - for (Path path : fullPath) { - if (path.toString().startsWith(".")) { - throw new DBException("Resource path '" + resourcePath + "' can't start with dot"); - } - } - if (!resourcePath.matches(regex)) { - String illegalCharacters = resourcePath.replaceAll(regex, " ").strip(); - throw new DBException("Resource path '" + resourcePath + "' contains illegal characters: " + illegalCharacters); + @NotNull + @Override + public String setResourceProperties( + @NotNull String projectId, + @NotNull String resourcePath, + @NotNull Map properties + ) throws DBException { + try (var ignoredLock = lockController.lock(projectId, "resourcePropertyUpdate")) { + validateResourcePath(resourcePath); + BaseWebProjectImpl webProject = getWebProject(projectId, false); + doFileWriteOperation(projectId, webProject.getMetadataFilePath(), + () -> { + log.debug("Updating resource '" + resourcePath + "' properties in project '" + projectId + "'"); + webProject.setResourceProperties(resourcePath, properties); + return null; + } + ); + return DEFAULT_CHANGE_ID; } } @@ -865,7 +924,7 @@ private Path getTargetPath(@NotNull String projectId, @NotNull String resourcePa if (!targetPath.startsWith(projectPath)) { throw new DBException("Invalid resource path"); } - return WebAppUtils.getWebApplication().getHomeDirectory().relativize(targetPath); + return targetPath; } catch (InvalidPathException e) { throw new DBException("Resource path contains invalid characters"); } @@ -878,7 +937,7 @@ private String makeProjectIdFromPath(Path path, RMProjectType type) { } @Nullable - private RMProject makeProjectFromId(String projectId, boolean loadPermissions) throws DBException { + protected RMProject makeProjectFromId(String projectId, boolean loadPermissions) throws DBException { var projectName = parseProjectName(projectId); var projectPath = getProjectPath(projectId); if (!Files.exists(projectPath)) { @@ -965,9 +1024,8 @@ protected T doProjectOperation(String projectId, RMFileOperation operatio fileHandler.projectOpened(projectId); } catch (Exception e) { if (credentialsProvider.getActiveUserCredentials() != null) { - WebAppUtils.getWebApplication().getEventController().addEvent( + ServletAppUtils.getServletApplication().getEventController().addEvent( new WSSessionLogUpdatedEvent( - WSEventType.SESSION_LOG_UPDATED, credentialsProvider.getActiveUserCredentials().getSmSessionId(), credentialsProvider.getActiveUserCredentials().getUserId(), MessageType.ERROR, @@ -984,9 +1042,8 @@ protected T doFileReadOperation(String projectId, Path file, RMFileOperation fileHandler.beforeFileRead(projectId, file); } catch (Exception e) { if (credentialsProvider.getActiveUserCredentials() != null) { - WebAppUtils.getWebApplication().getEventController().addEvent( + ServletAppUtils.getServletApplication().getEventController().addEvent( new WSSessionLogUpdatedEvent( - WSEventType.SESSION_LOG_UPDATED, credentialsProvider.getActiveUserCredentials().getSmSessionId(), credentialsProvider.getActiveUserCredentials().getUserId(), MessageType.ERROR, @@ -1094,7 +1151,7 @@ private RMResource makeResourceFromPath( ); } if (readProperties) { - final BaseProjectImpl project = (BaseProjectImpl) getWebProject(projectId, true); + final BaseProjectImpl project = getWebProject(projectId, true); final String resourcePath = getProjectRelativePath(projectId, path); final Map properties = project.getResourceProperties(resourcePath); @@ -1122,32 +1179,6 @@ private String getProjectRelativePath(@NotNull String projectId, @NotNull Path p return getProjectPath(projectId).toAbsolutePath().relativize(path).toString().replace('\\', IPath.SEPARATOR); } - private void fireRmResourceAddEvent(@NotNull String projectId, @NotNull String resourcePath) throws DBException { - RMEventManager.fireEvent( - new RMEvent(RMEvent.Action.RESOURCE_ADD, - getProject(projectId, false, false), - resourcePath) - ); - } - - private void fireRmResourceDeleteEvent(@NotNull String projectId, @NotNull String resourcePath) throws DBException { - RMEventManager.fireEvent( - new RMEvent(RMEvent.Action.RESOURCE_DELETE, - makeProjectFromId(projectId, false), - resourcePath - ) - ); - } - - private void fireRmProjectAddEvent(@NotNull RMProject project) throws DBException { - RMEventManager.fireEvent( - new RMEvent( - RMEvent.Action.RESOURCE_ADD, - project - ) - ); - } - protected void handleProjectOpened(String projectId) throws DBException { createResourceTypeFolders(getProjectPath(projectId)); } @@ -1160,16 +1191,24 @@ public static Builder builder( return new Builder(workspace, credentialsProvider, smControllerSupplier); } - public static final class Builder { - private final SMCredentialsProvider credentialsProvider; - private final Supplier smController; - private final DBPWorkspace workspace; + @Override + public String ping() { + return "pong (RM)"; + } + + public static class Builder { + protected final SMCredentialsProvider credentialsProvider; + protected final Supplier smController; + protected final DBPWorkspace workspace; - private Path rootPath; - private Path userProjectsPath; - private Path sharedProjectsPath; + protected Path rootPath; + protected Path userProjectsPath; + protected Path sharedProjectsPath; - private Builder(DBPWorkspace workspace, SMCredentialsProvider credentialsProvider, Supplier smControllerSupplier) { + protected Builder( + DBPWorkspace workspace, SMCredentialsProvider credentialsProvider, + Supplier smControllerSupplier + ) { this.workspace = workspace; this.credentialsProvider = credentialsProvider; this.smController = smControllerSupplier; @@ -1263,5 +1302,4 @@ public static boolean isProjectOwner(String projectId, String userId) { rmProjectName.name.equals(userId); } - } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperation.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperation.java index e61cda3393..38229523ff 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperation.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperation.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandler.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandler.java index 3a5ccedacd..1b5466aa88 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandler.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandlerDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandlerDescriptor.java index fe7e7e829d..521a0183ff 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandlerDescriptor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandlerDescriptor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandlersRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandlersRegistry.java index 8dd215bb56..308b4f6997 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandlersRegistry.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandlersRegistry.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java deleted file mode 100644 index 22f38d5284..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * 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.model.rm.lock; - -import com.google.gson.Gson; -import io.cloudbeaver.model.app.WebApplication; -import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.app.DBPWorkspace; - -import java.io.IOException; -import java.io.Reader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.UUID; - -/** - * File based resource locks - */ -public class RMFileLockController { - private static final Log log = Log.getLog(RMFileLockController.class); - private static final int DEFAULT_MAX_LOCK_TIME = 1 * 60 * 1000; // 1 min - private static final int CHECK_PERIOD = 10; - - private static final String LOCK_META_FOLDER = ".locks"; - private static final String LOCK_FILE_EXTENSION = ".lock"; - - private final Gson gson = new Gson(); - - private final Path lockFolderPath; - private final String applicationId; - private final int maxLockTime; - - - public RMFileLockController(WebApplication application) throws DBException { - this(application, DEFAULT_MAX_LOCK_TIME); - } - - // for tests - public RMFileLockController(WebApplication application, int maxLockTime) throws DBException { - this.lockFolderPath = application.getWorkspaceDirectory() - .resolve(DBPWorkspace.METADATA_FOLDER) - .resolve(LOCK_META_FOLDER); - this.applicationId = application.getApplicationInstanceId(); - this.maxLockTime = maxLockTime; - } - - /** - * Lock the project for the duration of any operation. - * Other threads/processes will also see this lock, and will wait for it to end - * or force intercept lock, if the operation will take too long and - * exceeds the maximum available locking time {@link #maxLockTime} or the lock is invalid {@link #awaitUnlock)}. - * - * @param projectId - project to be locked - * @param operationName - executed operation name - * @return - lock - */ - @NotNull - public RMLock lockProject(@NotNull String projectId,@NotNull String operationName) throws DBException { - synchronized (RMFileLockController.class) { - try { - createLockFolderIfNeeded(); - createProjectFolder(projectId); - Path projectLockFile = getProjectLockFilePath(projectId); - - RMLockInfo lockInfo = new RMLockInfo.Builder(projectId, UUID.randomUUID().toString()) - .setApplicationId(applicationId) - .setOperationName(operationName) - .setOperationStartTime(System.currentTimeMillis()) - .build(); - createLockFile(projectLockFile, lockInfo); - return new RMLock(projectLockFile); - } catch (Exception e) { - throw new DBException("Failed to lock project: " + projectId, e); - } - } - } - - /** - * if the project is already locked, the operation will be executed as a child of the first lock, - * otherwise it creates its own lock. - * - * @param projectId - project to be locked - * @param operationName - executed operation name - * @return - lock - */ - @Nullable - public RMLock lockIfNotLocked(@NotNull String projectId, @NotNull String operationName) throws DBException { - synchronized (RMFileLockController.class) { - if (isProjectLocked(projectId)) { - return null; - } - return lockProject(projectId, operationName); - } - } - - /** - * Check that project locked - */ - public boolean isProjectLocked(String projectId) { - Path projectLockFilePath = getProjectLockFilePath(projectId); - return isLocked(projectLockFilePath); - } - - protected boolean isLocked(Path lockFilePath) { - return Files.exists(lockFilePath); - } - - private void createLockFile(Path projectLockFile, RMLockInfo lockInfo) throws DBException, InterruptedException { - boolean lockFileCreated = false; - while (!lockFileCreated) { - if (Files.exists(projectLockFile)) { - awaitUnlock(lockInfo.getProjectId(), projectLockFile); - } - try { - Files.createFile(projectLockFile); - lockFileCreated = true; - } catch (IOException e) { - if (Files.exists(projectLockFile)) { - log.info("Looks like file was locked by another rm instance at the same time"); - continue; - } else { - throw new DBException("Failed to create lock file: " + projectLockFile, e); - } - } - - try { - Files.write(projectLockFile, gson.toJson(lockInfo).getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - log.error("Failed to write lock info, unlock project: " + lockInfo.getProjectId()); - try { - Files.deleteIfExists(lockFolderPath); - } catch (IOException ex) { - throw new DBException("Failed to remove invalid lock file: " + projectLockFile, ex); - } - throw new DBException("Failed to lock project: " + lockInfo.getProjectId(), e); - } - } - - } - - private void createProjectFolder(String projectId) throws DBException { - Path projectLocksFolder = lockFolderPath.resolve(projectId); - if (Files.exists(projectLocksFolder)) { - return; - } - try { - Files.createDirectories(projectLocksFolder); - } catch (Exception e) { - if (Files.exists(projectLocksFolder)) { - // ignore, because file can be created by another server - } else { - throw new DBException("Failed to create project lock folder: " + projectId, e); - } - } - } - - protected void awaitUnlock(String projectId, Path projectLockFile) throws InterruptedException, DBException { - if (!isLocked(projectLockFile)) { - return; - } - awaitingUnlock(projectId, projectLockFile); - } - - protected void awaitingUnlock(String projectId, Path projectLockFile) throws DBException, InterruptedException { - log.info("Waiting for a file to be unlocked: " + projectLockFile); - RMLockInfo originalLockInfo = readLockInfo(projectId, projectLockFile); - boolean fileUnlocked = originalLockInfo == null; //lock can be removed at the moment when we try to read lock file info - int maxIterations = maxLockTime / CHECK_PERIOD; - int currentCheckCount = 0; - - while (!fileUnlocked) { - fileUnlocked = !isLocked(projectLockFile); - if (currentCheckCount >= maxIterations || fileUnlocked) { - break; - } - if (originalLockInfo != null & originalLockInfo.isBlank()) { - // possible in situation where the project has just been locked - // and the lock information has not yet been written - originalLockInfo = readLockInfo(projectId, projectLockFile); - } - currentCheckCount++; - Thread.sleep(CHECK_PERIOD); - } - if (fileUnlocked) { - return; - } - - RMLockInfo currentLockInfo = readLockInfo(projectId, projectLockFile); - if (currentLockInfo == null) { - // file unlocked now - return; - } - - //checking that this is not a new lock from another operation - if (originalLockInfo.getOperationId().equals(currentLockInfo.getOperationId())) { - forceUnlock(projectLockFile); - } else { - awaitUnlock(projectId, lockFolderPath); - } - } - - protected void forceUnlock(Path projectLockFile) { - // something went wrong and lock is invalid - log.warn("File has not been unlocked within the expected period, force unlock"); - try { - Files.deleteIfExists(projectLockFile); - } catch (IOException e) { - log.error(e); - } - } - - @Nullable - /** - @return - - null if lock not exist; - - empty lock info if the lock has just been created and the information has not yet been written; - - lock info - */ - private RMLockInfo readLockInfo(String projectId, Path projectLockFile) throws DBException { - if (Files.notExists(projectLockFile)) { - return null; - } - try (Reader reader = Files.newBufferedReader(projectLockFile, StandardCharsets.UTF_8)) { - return gson.fromJson(reader, RMLockInfo.class); - } catch (IOException e) { - if (!isLocked(projectLockFile)) { - return null; - } - log.warn("Failed to read lock file info, but lock file still exist: " + projectLockFile); - return RMLockInfo.emptyLock(projectId); - } - } - - private Path getProjectLockFilePath(String projectId) { - return lockFolderPath.resolve(projectId).resolve(projectId + LOCK_FILE_EXTENSION); - } - - private void createLockFolderIfNeeded() throws IOException { - synchronized (RMFileLockController.class) { - if (Files.notExists(lockFolderPath)) { - Files.createDirectories(lockFolderPath); - } - } - } -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMLock.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMLock.java deleted file mode 100644 index 70afbd4630..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMLock.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.model.rm.lock; - -import org.jkiss.dbeaver.Log; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * Resource Manager resource lock - */ -public class RMLock implements AutoCloseable { - private static final Log log = Log.getLog(RMLock.class); - - private final Path lockFilePath; - - public RMLock(Path lockFilePath) { - this.lockFilePath = lockFilePath; - } - - /** - * Unlock resource and remove .lock file - */ - public void unlock() { - try { - Files.deleteIfExists(lockFilePath); - } catch (IOException e) { - log.error("Failed to unlock file: " + lockFilePath, e); - if (Files.exists(lockFilePath)) { - // file still locket, try to unlock again - unlock(); - } - } - } - - /** - * @return path to the lock file - */ - protected Path getLockFilePath() { - return lockFilePath; - } - - @Override - public void close() { - unlock(); - } -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMLockInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMLockInfo.java deleted file mode 100644 index 1cec6bbfa0..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMLockInfo.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.model.rm.lock; - -public class RMLockInfo { - private final String applicationId; - private final String projectId; - private final String operationId; - private final String operationName; - private final long operationStartTime; - - private RMLockInfo( - String applicationId, - String projectId, - String operationId, - String operationName, - long operationStartTime - ) { - this.applicationId = applicationId; - this.projectId = projectId; - this.operationId = operationId; - this.operationName = operationName; - this.operationStartTime = operationStartTime; - } - - static RMLockInfo emptyLock( - String projectId - ) { - return new RMLockInfo( - "", - projectId, - "", - "", - System.currentTimeMillis() - ); - } - - - public boolean isBlank() { - return operationId.isEmpty(); - } - - public String getApplicationId() { - return applicationId; - } - - public String getProjectId() { - return projectId; - } - - public String getOperationId() { - return operationId; - } - - public String getOperationName() { - return operationName; - } - - public long getOperationStartTime() { - return operationStartTime; - } - - public static final class Builder { - private String applicationId; - private final String projectId; - private final String operationId; - private String operationName; - private long operationStartTime; - - public Builder(String projectId, String operationId) { - this.projectId = projectId; - this.operationId = operationId; - } - - - public Builder setApplicationId(String applicationId) { - this.applicationId = applicationId; - return this; - } - - - public Builder setOperationName(String operationName) { - this.operationName = operationName; - return this; - } - - public Builder setOperationStartTime(long operationStartTime) { - this.operationStartTime = operationStartTime; - return this; - } - - public RMLockInfo build() { - return new RMLockInfo(applicationId, projectId, operationId, operationName, operationStartTime); - } - } -} 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..40a1e84267 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,8 +17,8 @@ package io.cloudbeaver.model.session; import io.cloudbeaver.model.WebServerMessage; -import io.cloudbeaver.model.app.WebApplication; -import io.cloudbeaver.model.app.WebAuthApplication; +import io.cloudbeaver.model.app.ServletApplication; +import io.cloudbeaver.model.app.ServletAuthApplication; import io.cloudbeaver.websocket.CBWebSessionEventHandler; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -29,12 +29,15 @@ import org.jkiss.dbeaver.model.auth.SMSessionContext; import org.jkiss.dbeaver.model.auth.impl.AbstractSessionPersistent; import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.security.user.SMTeam; 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; import java.time.LocalDateTime; import java.time.ZoneId; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -50,27 +53,33 @@ public abstract class BaseWebSession extends AbstractSessionPersistent { @NotNull protected final WebUserContext userContext; @NotNull - protected final WebAuthApplication application; + protected final ServletApplication application; protected volatile long lastAccessTime; private final List sessionEventHandlers = new CopyOnWriteArrayList<>(); private WebSessionEventsFilter eventsFilter = new WebSessionEventsFilter(); private final WebSessionWorkspace workspace; - public BaseWebSession(@NotNull String id, @NotNull WebAuthApplication application) throws DBException { + public BaseWebSession(@NotNull String id, @NotNull ServletApplication application) throws DBException { this.id = id; this.application = application; this.createTime = System.currentTimeMillis(); this.lastAccessTime = this.createTime; - this.workspace = new WebSessionWorkspace(this); + this.workspace = createWebWorkspace(); this.workspace.getAuthContext().addSession(this); this.userContext = createUserContext(); } + @NotNull + protected WebSessionWorkspace createWebWorkspace() { + return new WebSessionWorkspace(this); + } + protected WebUserContext createUserContext() throws DBException { return new WebUserContext(this.application, this.workspace); } + @NotNull public WebSessionWorkspace getWorkspace() { return workspace; } @@ -113,7 +122,15 @@ public synchronized boolean updateSMSession(SMAuthInfo smAuthInfo) throws DBExce public synchronized void refreshUserData() { try { userContext.refreshPermissions(); - userContext.refreshAccessibleProjects(); + if (userContext.isAuthorizedInSecurityManager()) { + userContext.refreshAccessibleProjects(); + if (userContext.getUser() != null) { + List userTeamIds = Arrays.stream(userContext.getSecurityController().getCurrentUserTeams()) + .map(SMTeam::getTeamId) + .toList(); + userContext.getUser().setTeams(userTeamIds.toArray(new String[0])); + } + } } catch (DBException e) { addSessionError(e); log.error("Error refreshing accessible projects", e); @@ -132,6 +149,11 @@ public SMSessionContext getSessionContext() { return workspace.getAuthContext(); } + protected void clearSessionContext() { + this.workspace.getAuthContext().clear(); + this.workspace.getAuthContext().addSession(this); + } + @NotNull @Property public String getSessionId() { @@ -139,7 +161,7 @@ public String getSessionId() { } @NotNull - public WebApplication getApplication() { + public ServletApplication getApplication() { return application; } @@ -165,19 +187,32 @@ public synchronized WebUserContext getUserContext() { @Override public void close() { super.close(); - var sessionExpiredEvent = new WSSessionExpiredEvent(); + cleanUpSession(true); + } + + public void close(boolean clearTokens, boolean sendSessionExpiredEvent) { + cleanUpSession(sendSessionExpiredEvent); + } + + private void cleanUpSession(boolean sendSessionExpiredEvent) { + application.getEventController().addEvent(new WSEventDeleteTempFile(getSessionId())); synchronized (sessionEventHandlers) { + var sessionExpiredEvent = new WSSessionExpiredEvent(); for (CBWebSessionEventHandler sessionEventHandler : sessionEventHandlers) { - try { - sessionEventHandler.handleWebSessionEvent(sessionExpiredEvent); - } catch (DBException e) { - log.warn("Failed to send session expiration event", e); + if (sendSessionExpiredEvent) { + try { + sessionEventHandler.handleWebSessionEvent(sessionExpiredEvent); + } catch (DBException e) { + log.warn("Failed to send session expiration event", e); + } } sessionEventHandler.close(); } sessionEventHandlers.clear(); workspace.dispose(); + + clearSessionContext(); } } @@ -211,6 +246,9 @@ public boolean isValid() { @Property public long getRemainingTime() { - return application.getMaxSessionIdleTime() + lastAccessTime - System.currentTimeMillis(); + if (application instanceof ServletAuthApplication authApplication) { + return authApplication.getMaxSessionIdleTime() + lastAccessTime - System.currentTimeMillis(); + } + return Integer.MAX_VALUE; } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebActionParameters.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebActionParameters.java index 3753cd1b01..c7a681266a 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebActionParameters.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebActionParameters.java @@ -1,18 +1,18 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp + * Copyright (C) 2010-2024 DBeaver Corp and others * - * All Rights Reserved. + * 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 * - * 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. + * 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.model.session; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAsyncTaskProcessor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAsyncTaskProcessor.java index 6618006083..52a39e7049 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAsyncTaskProcessor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAsyncTaskProcessor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java index eefd83a59d..e02f4e779b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -22,6 +22,7 @@ import io.cloudbeaver.registry.WebAuthProviderDescriptor; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.auth.SMAuthInfo; import org.jkiss.dbeaver.model.auth.SMAuthProvider; import org.jkiss.dbeaver.model.auth.SMSession; import org.jkiss.dbeaver.model.auth.SMSessionPrincipal; @@ -33,7 +34,8 @@ /** * WebAuthInfo */ -public class WebAuthInfo implements SMSessionPrincipal { +//TODO: create serializable model? +public class WebAuthInfo implements SMSessionPrincipal, WebUserAuthToken { private static final Log log = Log.getLog(WebAuthInfo.class); @@ -41,7 +43,10 @@ public class WebAuthInfo implements SMSessionPrincipal { private final WebUser user; private final WebAuthProviderDescriptor authProvider; private String authProviderConfigurationId; - private SMSession authSession; + @NotNull + private final SMAuthInfo authInfo; + @NotNull + private final SMSession authSession; private final OffsetDateTime loginTime; private final DBWUserIdentity userIdentity; private String message; @@ -54,11 +59,14 @@ public WebAuthInfo( @NotNull WebAuthProviderDescriptor authProvider, @NotNull DBWUserIdentity userIdentity, @NotNull SMSession authSession, - @NotNull OffsetDateTime loginTime) { + @NotNull SMAuthInfo authInfo, + @NotNull OffsetDateTime loginTime + ) { this.session = session; this.user = user; this.authProvider = authProvider; this.userIdentity = userIdentity; + this.authInfo = authInfo; this.authSession = authSession; this.loginTime = loginTime; } @@ -119,10 +127,16 @@ public WebAuthProviderDescriptor getAuthProviderDescriptor() { return authProvider; } + @NotNull public SMSession getAuthSession() { return authSession; } + @NotNull + public SMAuthInfo getAuthInfo() { + return authInfo; + } + void closeAuth() { if (authProvider != null && authSession != null) { try { @@ -130,8 +144,6 @@ void closeAuth() { authProviderInstance.closeSession(session, authSession); } catch (Exception e) { log.error(e); - } finally { - authSession = null; } } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java index c71e55aa19..9bd8cb4261 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,8 +17,9 @@ package io.cloudbeaver.model.session; import io.cloudbeaver.model.WebServerMessage; -import io.cloudbeaver.model.app.WebAuthApplication; +import io.cloudbeaver.model.app.ServletAuthApplication; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.auth.SMSessionPrincipal; @@ -28,7 +29,7 @@ public class WebHeadlessSession extends BaseWebSession { public WebHeadlessSession( @NotNull String id, - @NotNull WebAuthApplication application + @NotNull ServletAuthApplication application ) throws DBException { super(id, application); } @@ -43,6 +44,7 @@ public void addSessionMessage(WebServerMessage message) { } + @Nullable @Override public SMSessionPrincipal getSessionPrincipal() { return null; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java new file mode 100644 index 0000000000..7aeb977db7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java @@ -0,0 +1,71 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.session; + +import jakarta.servlet.http.HttpServletRequest; +import org.jkiss.code.Nullable; + +public class WebHttpRequestInfo { + public static final String USER_AGENT = "User-Agent"; + + @Nullable + private final String id; + @Nullable + private final Object locale; + @Nullable + private final String lastRemoteAddress; + @Nullable + private final String lastRemoteUserAgent; + + public WebHttpRequestInfo(HttpServletRequest request) { + this.id = request.getSession() == null ? null : request.getSession().getId(); + this.locale = request.getAttribute("locale"); + this.lastRemoteAddress = request.getRemoteAddr(); + this.lastRemoteUserAgent = request.getHeader(USER_AGENT); + } + + public WebHttpRequestInfo( + @Nullable String id, + @Nullable Object locale, + @Nullable String lastRemoteAddress, + @Nullable String lastRemoteUserAgent + ) { + this.id = id; + this.locale = locale; + this.lastRemoteAddress = lastRemoteAddress; + this.lastRemoteUserAgent = lastRemoteUserAgent; + } + + public String getId() { + return id; + } + + @Nullable + public Object getLocale() { + return locale; + } + + @Nullable + public String getLastRemoteAddress() { + return lastRemoteAddress; + } + + @Nullable + public String getLastRemoteUserAgent() { + return lastRemoteUserAgent; + } +} 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..30e926ac38 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,23 +16,20 @@ */ package io.cloudbeaver.model.session; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.InstanceCreator; -import io.cloudbeaver.DBWConstants; -import io.cloudbeaver.DBWebException; -import io.cloudbeaver.DataSourceFilter; -import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.*; +import io.cloudbeaver.model.CustomCancelableJob; import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.WebServerMessage; -import io.cloudbeaver.model.app.WebAuthApplication; +import io.cloudbeaver.model.app.ServletApplication; +import io.cloudbeaver.model.app.ServletAuthApplication; +import io.cloudbeaver.model.session.monitor.TaskProgressMonitor; import io.cloudbeaver.model.user.WebUser; import io.cloudbeaver.service.DBWSessionHandler; import io.cloudbeaver.service.sql.WebSQLConstants; import io.cloudbeaver.utils.CBModelConstants; -import io.cloudbeaver.utils.WebAppUtils; import io.cloudbeaver.utils.WebDataSourceUtils; +import io.cloudbeaver.utils.WebEventUtils; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; @@ -42,12 +39,9 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBFileController; import org.jkiss.dbeaver.model.DBPDataSourceContainer; -import org.jkiss.dbeaver.model.DBPEvent; +import org.jkiss.dbeaver.model.DBPEventListener; import org.jkiss.dbeaver.model.access.DBAAuthCredentials; import org.jkiss.dbeaver.model.access.DBACredentialsProvider; -import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; -import org.jkiss.dbeaver.model.app.DBPDataSourceRegistryCache; -import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.auth.*; import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; import org.jkiss.dbeaver.model.exec.DBCException; @@ -62,57 +56,47 @@ import org.jkiss.dbeaver.model.runtime.AbstractJob; import org.jkiss.dbeaver.model.runtime.BaseProgressMonitor; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.model.runtime.ProxyProgressMonitor; import org.jkiss.dbeaver.model.security.SMAdminController; import org.jkiss.dbeaver.model.security.SMConstants; import org.jkiss.dbeaver.model.security.SMController; -import org.jkiss.dbeaver.model.security.SMObjectType; -import org.jkiss.dbeaver.model.security.user.SMObjectPermissions; import org.jkiss.dbeaver.model.sql.DBQuotaException; import org.jkiss.dbeaver.model.websocket.event.MessageType; -import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.WSSessionLogUpdatedEvent; -import org.jkiss.dbeaver.registry.DataSourceDescriptor; import org.jkiss.dbeaver.runtime.DBWorkbench; -import org.jkiss.dbeaver.runtime.jobs.DisconnectJob; import org.jkiss.utils.CommonUtils; import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; 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 */ +//TODO: split to authenticated and non authenticated context public class WebSession extends BaseWebSession - implements SMSession, SMCredentialsProvider, DBACredentialsProvider, IAdaptable { + implements SMSessionWithAuth, SMCredentialsProvider, DBACredentialsProvider, IAdaptable { private static final Log log = Log.getLog(WebSession.class); 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(); + public static String RUNTIME_PARAM_AUTH_INFOS = "auth-infos"; private final AtomicInteger taskCount = new AtomicInteger(); - private String lastRemoteAddr; + private final String lastRemoteAddr; private String lastRemoteUserAgent; - private Set accessibleConnectionIds = Collections.emptySet(); - private String locale; private boolean cacheExpired; - private final Map connections = new HashMap<>(); + protected WebSessionGlobalProjectImpl globalProject; private final List sessionMessages = new ArrayList<>(); private final Map asyncTasks = new HashMap<>(); @@ -124,18 +108,46 @@ public class WebSession extends BaseWebSession private DBNModel navigatorModel; private final DBRProgressMonitor progressMonitor = new SessionProgressMonitor(); private final Map sessionHandlers; + private final WebDataSourceConnectEventListener connectListener = new WebDataSourceConnectEventListener(this); public WebSession( - @NotNull HttpSession httpSession, - @NotNull WebAuthApplication application, + @NotNull WebHttpRequestInfo requestInfo, + @NotNull ServletAuthApplication application, @NotNull Map sessionHandlers ) throws DBException { - super(httpSession.getId(), application); + this(requestInfo.getId(), + CommonUtils.toString(requestInfo.getLocale()), + application, + sessionHandlers, + requestInfo.getLastRemoteAddress() + ); + updateSessionParameters(requestInfo); + } + + protected WebSession( + @NotNull String id, + @Nullable String locale, + @NotNull ServletApplication application, + @NotNull Map sessionHandlers, + @NotNull String remoteAddr + ) throws DBException { + super(id, application); + if (CommonUtils.isEmpty(remoteAddr)) { + throw new DBException("Remote address cannot be empty"); + } + this.lastRemoteAddr = remoteAddr; this.lastAccessTime = this.createTime; - setLocale(CommonUtils.toString(httpSession.getAttribute(ATTR_LOCALE), this.locale)); this.sessionHandlers = sessionHandlers; + setLocale(CommonUtils.toString(locale, this.locale)); + //force authorization of anonymous session to avoid access error, + //because before authorization could be called by any request, + //but now 'updateInfo' is called only in special requests, + //and the order of requests is not guaranteed. + //look at CB-4747 + refreshSessionAuth(); } + @Nullable @Override public SMSessionPrincipal getSessionPrincipal() { synchronized (authTokens) { @@ -146,8 +158,8 @@ public SMSessionPrincipal getSessionPrincipal() { } } - @NotNull - public DBPProject getSingletonProject() { + @Nullable + public WebSessionProjectImpl getSingletonProject() { return getWorkspace().getActiveProject(); } @@ -244,65 +256,6 @@ public synchronized void refreshUserData() { initNavigatorModel(); } - /** - * updates data sources based on event in web session - * - * @param project project of connection - * @param dataSourceIds list of updated connections - * @param type type of event - */ - public synchronized boolean updateProjectDataSources( - DBPProject project, - List dataSourceIds, - WSEventType type - ) { - var sendDataSourceUpdatedEvent = false; - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - // save old connections - var oldDataSources = dataSourceIds.stream() - .map(registry::getDataSource) - .filter(Objects::nonNull) - .collect(Collectors.toMap( - DBPDataSourceContainer::getId, - ds -> new DataSourceDescriptor((DataSourceDescriptor) ds, ds.getRegistry()) - )); - if (type == WSEventType.DATASOURCE_CREATED || type == WSEventType.DATASOURCE_UPDATED) { - registry.refreshConfig(dataSourceIds); - } - for (String dsId : dataSourceIds) { - DataSourceDescriptor ds = (DataSourceDescriptor) registry.getDataSource(dsId); - if (ds == null) { - continue; - } - switch (type) { - case DATASOURCE_CREATED: { - WebConnectionInfo connectionInfo = new WebConnectionInfo(this, ds); - this.connections.put(connectionInfo.getId(), connectionInfo); - sendDataSourceUpdatedEvent = true; - break; - } - case DATASOURCE_UPDATED: { - // if settings were changed we need to send event - sendDataSourceUpdatedEvent |= !ds.equalSettings(oldDataSources.get(dsId)); - break; - } - case DATASOURCE_DELETED: { - WebDataSourceUtils.disconnectDataSource(this, ds); - if (registry instanceof DBPDataSourceRegistryCache) { - ((DBPDataSourceRegistryCache) registry).removeDataSourceFromList(ds); - } - this.connections.remove(ds.getId()); - sendDataSourceUpdatedEvent = true; - break; - } - default: { - break; - } - } - } - return sendDataSourceUpdatedEvent; - } - // Note: for admin use only public synchronized void resetUserState() throws DBException { clearAuthTokens(); @@ -313,6 +266,12 @@ public synchronized void resetUserState() throws DBException { log.error(e); } refreshUserData(); + clearSessionContext(); + } + + @NotNull + public DBPEventListener getDataSourceConnectListener() { + return connectListener; } private void initNavigatorModel() { @@ -322,7 +281,7 @@ private void initNavigatorModel() { this.navigatorModel.dispose(); this.navigatorModel = null; } - this.connections.clear(); + this.globalProject = null; loadProjects(); @@ -342,14 +301,13 @@ private void loadProjects() { // No anonymous mode in distributed apps return; } - refreshAccessibleConnectionIds(); try { RMController controller = getRmController(); RMProject[] rmProjects = controller.listAccessibleProjects(); for (RMProject project : rmProjects) { createWebProject(project); } - if (user == null) { + if (user == null && application.getAppConfiguration().isAnonymousAccessEnabled()) { WebProjectImpl anonymousProject = createWebProject(RMUtils.createAnonymousProject()); anonymousProject.setInMemory(true); } @@ -362,57 +320,41 @@ private void loadProjects() { } } - public WebProjectImpl createWebProject(RMProject project) { - // Do not filter data sources from user project - DataSourceFilter filter = project.getType() == RMProjectType.GLOBAL - ? this::isDataSourceAccessible - : x -> true; - WebProjectImpl sessionProject = application.createProjectImpl(this, project, filter); + private WebSessionProjectImpl createWebProject(RMProject project) throws DBException { + WebSessionProjectImpl sessionProject; + if (project.isGlobal()) { + sessionProject = createGlobalProject(project); + } else { + sessionProject = createSessionProject(project); + } // do not load data sources for anonymous project if (project.getType() == RMProjectType.USER && userContext.getUser() == null) { sessionProject.setInMemory(true); } - DBPDataSourceRegistry dataSourceRegistry = sessionProject.getDataSourceRegistry(); - dataSourceRegistry.setAuthCredentialsProvider(this); addSessionProject(sessionProject); if (!project.isShared() || application.isConfigurationMode()) { getWorkspace().setActiveProject(sessionProject); } - for (DBPDataSourceContainer ds : dataSourceRegistry.getDataSources()) { - addConnection(new WebConnectionInfo(this, ds)); - } - Throwable lastError = dataSourceRegistry.getLastError(); - if (lastError != null) { - addSessionError(lastError); - log.error("Error refreshing connections from project '" + project.getId() + "'", lastError); - } + log.info(String.format( + "Project created: [ID=%s, Name=%s, Type=%s, Creator=%s]", + project.getId(), project.getName(), project.getType(), project.getCreator() + )); return sessionProject; } - public void filterAccessibleConnections(List connections) { - connections.removeIf(c -> !isDataSourceAccessible(c.getDataSourceContainer())); + protected WebSessionProjectImpl createSessionProject(@NotNull RMProject project) throws DBException { + return new WebSessionProjectImpl(this, project, getProjectPath(project)); } - private boolean isDataSourceAccessible(DBPDataSourceContainer dataSource) { - return dataSource.isExternallyProvided() || - dataSource.isTemporary() || - this.hasPermission(DBWConstants.PERMISSION_ADMIN) || - accessibleConnectionIds.contains(dataSource.getId()); + @NotNull + protected Path getProjectPath(@NotNull RMProject project) throws DBException { + return RMUtils.getProjectPath(project); } - @NotNull - private Set readAccessibleConnectionIds() { - try { - return getSecurityController() - .getAllAvailableObjectsPermissions(SMObjectType.datasource) - .stream() - .map(SMObjectPermissions::getObjectId) - .collect(Collectors.toSet()); - } catch (DBException e) { - addSessionError(e); - log.error("Error reading connection grants", e); - return Collections.emptySet(); - } + protected WebSessionProjectImpl createGlobalProject(RMProject project) { + globalProject = new WebSessionGlobalProjectImpl(this, project); + globalProject.refreshAccessibleConnectionIds(); + return globalProject; } private void resetSessionCache() throws DBCException { @@ -430,17 +372,7 @@ private void resetSessionCache() throws DBCException { } private void resetNavigationModel() { - Map conCopy; - synchronized (this.connections) { - conCopy = new HashMap<>(this.connections); - this.connections.clear(); - } - - for (WebConnectionInfo connectionInfo : conCopy.values()) { - if (connectionInfo.isConnected()) { - new DisconnectJob(connectionInfo.getDataSourceContainer()).schedule(); - } - } + getWorkspace().getProjects().forEach(WebSessionProjectImpl::dispose); if (this.navigatorModel != null) { this.navigatorModel.dispose(); @@ -454,7 +386,9 @@ private synchronized void refreshSessionAuth() { authAsAnonymousUser(); } else if (getUserId() != null) { userContext.refreshPermissions(); - refreshAccessibleConnectionIds(); + if (globalProject != null) { + globalProject.refreshAccessibleConnectionIds(); + } } } catch (Exception e) { @@ -463,32 +397,6 @@ private synchronized void refreshSessionAuth() { } } - private synchronized void refreshAccessibleConnectionIds() { - this.accessibleConnectionIds = readAccessibleConnectionIds(); - } - - public synchronized void addAccessibleConnectionToCache(@NotNull String dsId) { - this.accessibleConnectionIds.add(dsId); - var registry = getProjectById(WebAppUtils.getGlobalProjectId()).getDataSourceRegistry(); - var dataSource = registry.getDataSource(dsId); - if (dataSource != null) { - connections.put(dsId, new WebConnectionInfo(this, dataSource)); - // reflect changes is navigator model - registry.notifyDataSourceListeners(new DBPEvent(DBPEvent.Action.OBJECT_ADD, dataSource, true)); - } - } - - public synchronized void removeAccessibleConnectionFromCache(@NotNull String dsId) { - var registry = getProjectById(WebAppUtils.getGlobalProjectId()).getDataSourceRegistry(); - var dataSource = registry.getDataSource(dsId); - if (dataSource != null) { - this.accessibleConnectionIds.remove(dsId); - connections.remove(dsId); - // reflect changes is navigator model - registry.notifyDataSourceListeners(new DBPEvent(DBPEvent.Action.OBJECT_REMOVE, dataSource)); - dataSource.dispose(); - } - } private synchronized void authAsAnonymousUser() throws DBException { if (!application.getAppConfiguration().isAnonymousAccessEnabled()) { @@ -508,10 +416,18 @@ public void setLocale(@Nullable String locale) { this.locale = locale != null ? locale : Locale.getDefault().getLanguage(); } + @Nullable public DBNModel getNavigatorModel() { return navigatorModel; } + @NotNull + public DBNModel getNavigatorModelOrThrow() throws DBWebException { + if (navigatorModel != null) { + return navigatorModel; + } + throw new DBWebException("Navigator model is not found in session"); + } /** * Returns and clears progress messages */ @@ -524,16 +440,10 @@ public List getSessionMessages() { } } - public synchronized void updateInfo( - HttpServletRequest request, - HttpServletResponse response - ) throws DBWebException { + public synchronized void updateInfo(boolean isOldHttpSessionUsed) { + log.debug("Update session lifetime " + getSessionId() + " for user " + getUserId()); touchSession(); - HttpSession httpSession = request.getSession(); - this.lastRemoteAddr = request.getRemoteAddr(); - this.lastRemoteUserAgent = request.getHeader("User-Agent"); - this.cacheExpired = false; - if (!httpSession.isNew()) { + if (isOldHttpSessionUsed) { try { // Persist session if (!isAuthorizedInSecurityManager()) { @@ -553,55 +463,9 @@ public synchronized void updateInfo( } } - @Association - public List getConnections() { - synchronized (connections) { - return new ArrayList<>(connections.values()); - } - } - - @NotNull - public WebConnectionInfo getWebConnectionInfo(@Nullable String projectId, String connectionID) throws DBWebException { - WebConnectionInfo connectionInfo; - synchronized (connections) { - connectionInfo = connections.get(connectionID); - } - if (connectionInfo == null) { - WebProjectImpl project = getProjectById(projectId); - if (project == null) { - throw new DBWebException("Project '" + projectId + "' not found in web workspace"); - } - DBPDataSourceContainer dataSource = project.getDataSourceRegistry().getDataSource(connectionID); - if (dataSource != null) { - connectionInfo = new WebConnectionInfo(this, dataSource); - synchronized (connections) { - connections.put(connectionID, connectionInfo); - } - } else { - throw new DBWebException("Connection '" + connectionID + "' not found"); - } - } - return connectionInfo; - } - - @Nullable - public WebConnectionInfo findWebConnectionInfo(String connectionID) { - synchronized (connections) { - return connections.get(connectionID); - } - } - - public void addConnection(WebConnectionInfo connectionInfo) { - synchronized (connections) { - connections.put(connectionInfo.getId(), connectionInfo); - } - } - - public void removeConnection(WebConnectionInfo connectionInfo) { - connectionInfo.clearCache(); - synchronized (connections) { - connections.remove(connectionInfo.getId()); - } + public synchronized void updateSessionParameters(WebHttpRequestInfo requestInfo) { + this.lastRemoteUserAgent = requestInfo.getLastRemoteUserAgent(); + this.cacheExpired = false; } @Override @@ -621,7 +485,26 @@ public void close() { super.close(); } - private void clearAuthTokens() throws DBException { + @Override + public void close(boolean clearTokens, boolean sendSessionExpiredEvent) { + try { + resetNavigationModel(); + resetSessionCache(); + } catch (Throwable e) { + log.error(e); + } + if (clearTokens) { + try { + clearAuthTokens(); + } catch (Exception e) { + log.error("Error closing web session tokens"); + } + } + this.userContext.setUser(null); + super.close(clearTokens, sendSessionExpiredEvent); + } + + private List clearAuthTokens() throws DBException { ArrayList tokensCopy; synchronized (authTokens) { tokensCopy = new ArrayList<>(this.authTokens); @@ -630,6 +513,7 @@ private void clearAuthTokens() throws DBException { removeAuthInfo(ai); } resetAuthToken(); + return tokensCopy; } public DBRProgressMonitor getProgressMonitor() { @@ -639,7 +523,7 @@ public DBRProgressMonitor getProgressMonitor() { /////////////////////////////////////////////////////// // Async model - public WebAsyncTaskInfo getAsyncTask(String taskId, String taskName, boolean create) { + public WebAsyncTaskInfo getAsyncTask(@NotNull String taskId, @NotNull String taskName, boolean create) { synchronized (asyncTasks) { WebAsyncTaskInfo taskInfo = asyncTasks.get(taskId); if (taskInfo == null && create) { @@ -650,13 +534,13 @@ public WebAsyncTaskInfo getAsyncTask(String taskId, String taskName, boolean cre } } + @NotNull public WebAsyncTaskInfo asyncTaskStatus(String taskId, boolean removeOnFinish) throws DBWebException { synchronized (asyncTasks) { WebAsyncTaskInfo taskInfo = asyncTasks.get(taskId); if (taskInfo == null) { throw new DBWebException("Task '" + taskId + "' not found"); } - taskInfo.setRunning(taskInfo.getJob() != null && !taskInfo.getJob().isFinished()); if (removeOnFinish && !taskInfo.isRunning()) { asyncTasks.remove(taskId); } @@ -673,22 +557,44 @@ public boolean asyncTaskCancel(String taskId) throws DBWebException { } } AbstractJob job = taskInfo.getJob(); + if (job instanceof CustomCancelableJob cancelableJob) { + cancelableJob.cancelJob(this, taskInfo); + } if (job != null) { job.cancel(); } return true; } - public WebAsyncTaskInfo createAndRunAsyncTask(String taskName, WebAsyncTaskProcessor runnable) { + + public WebAsyncTaskInfo createAsyncTask(@NotNull String taskName) { int taskId = TASK_ID.incrementAndGet(); WebAsyncTaskInfo asyncTask = getAsyncTask(String.valueOf(taskId), taskName, true); + return asyncTask; + } + + public List findTasksByJob(@NotNull Class jobClass) { + synchronized (asyncTasks) { + List result = new ArrayList<>(); + for (WebAsyncTaskInfo task : asyncTasks.values()) { + if (task.getJob() != null && jobClass.isAssignableFrom(task.getJob().getClass())) { + result.add(task); + } + } + return result; + } + } + + public WebAsyncTaskInfo createAndRunAsyncTask(@NotNull String taskName, @NotNull WebAsyncTaskProcessor runnable) { + WebAsyncTaskInfo asyncTask = createAsyncTask(taskName); AbstractJob job = new AbstractJob(taskName) { @Override protected IStatus run(DBRProgressMonitor monitor) { int curTaskCount = taskCount.incrementAndGet(); - TaskProgressMonitor taskMonitor = new TaskProgressMonitor(monitor, asyncTask); + DBRProgressMonitor taskMonitor = new TaskProgressMonitor(monitor, WebSession.this, asyncTask); + try { Number queryLimit = application.getAppConfiguration().getResourceQuota(WebSQLConstants.QUOTA_PROP_QUERY_LIMIT); if (queryLimit != null && curTaskCount > queryLimit.intValue()) { @@ -699,8 +605,7 @@ protected IStatus run(DBRProgressMonitor monitor) { runnable.run(taskMonitor); asyncTask.setResult(runnable.getResult()); asyncTask.setExtendedResult(runnable.getExtendedResults()); - asyncTask.setStatus("Finished"); - asyncTask.setRunning(false); + asyncTask.setStatus(DBWConstants.TASK_STATUS_FINISHED); } catch (InvocationTargetException e) { addSessionError(e.getTargetException()); asyncTask.setJobError(e.getTargetException()); @@ -708,6 +613,8 @@ protected IStatus run(DBRProgressMonitor monitor) { asyncTask.setJobError(e); } finally { taskCount.decrementAndGet(); + asyncTask.setRunning(false); + WebEventUtils.sendAsyncTaskEvent(WebSession.this, asyncTask); } return Status.OK_STATUS; } @@ -728,10 +635,9 @@ public void addSessionMessage(WebServerMessage message) { sessionMessages.add(message); } addSessionEvent(new WSSessionLogUpdatedEvent( - WSEventType.SESSION_LOG_UPDATED, this.userContext.getSmSessionId(), this.userContext.getUserId(), - MessageType.ERROR, + message.getType(), message.getMessage())); } @@ -766,8 +672,8 @@ public List readLog(Integer maxEntries, Boolean clearLog) { public T getAttribute(String name) { synchronized (attributes) { Object value = attributes.get(name); - if (value instanceof PersistentAttribute) { - value = ((PersistentAttribute) value).getValue(); + if (value instanceof PersistentAttribute persistentAttribute) { + value = persistentAttribute.value(); } return (T) value; } @@ -782,8 +688,8 @@ public void setAttribute(String name, Object value, boolean persistent) { public T getAttribute(String name, Function creator, Function disposer) { synchronized (attributes) { Object value = attributes.get(name); - if (value instanceof PersistentAttribute) { - value = ((PersistentAttribute) value).getValue(); + if (value instanceof PersistentAttribute persistentAttribute) { + value = persistentAttribute.value(); } if (value == null) { value = creator.apply(null); @@ -819,6 +725,14 @@ public WebAuthInfo getAuthInfo(@Nullable String providerID) { } } + @Override + public List getAuthInfos() { + synchronized (authTokens) { + return authTokens.stream().map(WebAuthInfo::getAuthInfo).toList(); + } + } + + public List getAllAuthInfo() { synchronized (authTokens) { return new ArrayList<>(authTokens); @@ -880,18 +794,23 @@ private void removeAuthInfo(WebAuthInfo oldAuthInfo) { } } - public void removeAuthInfo(String providerId) throws DBException { + public List removeAuthInfo(String providerId) throws DBException { + List oldInfo; if (providerId == null) { - clearAuthTokens(); + oldInfo = clearAuthTokens(); } else { WebAuthInfo authInfo = getAuthInfo(providerId); if (authInfo != null) { removeAuthInfo(authInfo); + oldInfo = List.of(authInfo); + } else { + oldInfo = List.of(); } } if (authTokens.isEmpty()) { resetUserState(); } + return oldInfo; } public List getContextCredentialsProviders() { @@ -911,24 +830,22 @@ public boolean provideAuthParameters( for (DBACredentialsProvider contextCredentialsProvider : getContextCredentialsProviders()) { contextCredentialsProvider.provideAuthParameters(monitor, dataSourceContainer, configuration); } + configuration.setRuntimeAttribute(RUNTIME_PARAM_AUTH_INFOS, getAllAuthInfo()); - WebConnectionInfo webConnectionInfo = findWebConnectionInfo(dataSourceContainer.getId()); - if (webConnectionInfo != null) { - WebDataSourceUtils.saveCredentialsInDataSource(webConnectionInfo, dataSourceContainer, configuration); + WebSessionProjectImpl project = getProjectById(dataSourceContainer.getProject().getId()); + if (project != null) { + WebConnectionInfo webConnectionInfo = project.findWebConnectionInfo(dataSourceContainer.getId()); + if (webConnectionInfo != null) { + WebDataSourceUtils.saveCredentialsInDataSource(webConnectionInfo, dataSourceContainer, configuration); + } } // uncommented because we had the problem with non-native auth models // (for example, can't connect to DynamoDB if credentials are not saved) DBAAuthCredentials credentials = configuration.getAuthModel().loadCredentials(dataSourceContainer, configuration); + WebDataSourceUtils.updateCredentialsFromProperties(credentials, configuration.getAuthProperties()); - InstanceCreator credTypeAdapter = type -> credentials; - Gson credGson = new GsonBuilder() - .setLenient() - .registerTypeAdapter(credentials.getClass(), credTypeAdapter) - .create(); - - credGson.fromJson(credGson.toJsonTree(configuration.getAuthProperties()), credentials.getClass()); - configuration.getAuthModel().saveCredentials(dataSourceContainer, configuration, credentials); + configuration.getAuthModel().provideCredentials(dataSourceContainer, configuration, credentials); } catch (DBException e) { addSessionError(e); log.error(e); @@ -967,9 +884,7 @@ public List getAdapters(Class adapter) { private boolean isAuthInfoInstanceOf(WebAuthInfo authInfo, Class adapter) { if (authInfo != null && authInfo.getAuthSession() != null) { - if (adapter.isInstance(authInfo.getAuthSession())) { - return true; - } + return adapter.isInstance(authInfo.getAuthSession()); } return false; } @@ -1007,23 +922,50 @@ public void refreshSMSession() throws DBException { } @Nullable - public WebProjectImpl getProjectById(@Nullable String projectId) { + public WebSessionProjectImpl getProjectById(@Nullable String projectId) { return getWorkspace().getProjectById(projectId); } - public List getAccessibleProjects() { + /** + * Returns project info from session cache. + * + * @throws DBWebException if project with provided id is not found. + */ + public WebSessionProjectImpl getAccessibleProjectById(@Nullable String projectId) throws DBWebException { + WebSessionProjectImpl project = null; + if (projectId != null) { + project = getWorkspace().getProjectById(projectId); + } + if (project == null) { + throw new DBWebException("Project not found: " + projectId); + } + return project; + } + + public List getAccessibleProjects() { return getWorkspace().getProjects(); } - public void addSessionProject(@NotNull WebProjectImpl project) { + /** + * Adds project to session cache and navigator tree. + */ + public void addSessionProject(@NotNull WebSessionProjectImpl project) { getWorkspace().addProject(project); if (navigatorModel != null) { navigatorModel.getRoot().addProject(project, false); } } - public void deleteSessionProject(@Nullable WebProjectImpl project) { + /** + * Removes project from session cache and navigator tree. + */ + public void deleteSessionProject(@Nullable WebSessionProjectImpl project) { if (project != null) { + RMProject rmProject = project.getRMProject(); + log.info(String.format( + "Project deleted: [ID=%s, Name=%s, Type=%s, Creator=%s]", + rmProject.getId(), rmProject.getName(), rmProject.getType(), rmProject.getCreator() + )); project.dispose(); } getWorkspace().removeProject(project); @@ -1047,10 +989,6 @@ public void removeSessionProject(@Nullable String projectId) throws DBException return; } deleteSessionProject(project); - var projectConnections = project.getDataSourceRegistry().getDataSources(); - for (DBPDataSourceContainer c : projectConnections) { - removeConnection(new WebConnectionInfo(this, c)); - } } @NotNull @@ -1062,6 +1000,16 @@ public DBFFileSystemManager getFileSystemManager(String projectId) throws DBExce return project.getFileSystemManager(); } + @NotNull + public WebSessionPreferenceStore getUserPreferenceStore() { + return getUserContext().getPreferenceStore(); + } + + @Nullable + public WebSessionGlobalProjectImpl getGlobalProject() { + return globalProject; + } + private class SessionProgressMonitor extends BaseProgressMonitor { @Override public void beginTask(String name, int totalWork) { @@ -1074,37 +1022,10 @@ public void subTask(String name) { } } - private static class TaskProgressMonitor extends ProxyProgressMonitor { - - private final WebAsyncTaskInfo asyncTask; - - public TaskProgressMonitor(DBRProgressMonitor original, WebAsyncTaskInfo asyncTask) { - super(original); - this.asyncTask = asyncTask; - } - - @Override - public void beginTask(String name, int totalWork) { - super.beginTask(name, totalWork); - asyncTask.setStatus(name); - } - - @Override - public void subTask(String name) { - super.subTask(name); - asyncTask.setStatus(name); - } + public boolean hasGlobalPermission(String permissionId) { + return true; } - private static class PersistentAttribute { - private final Object value; - - public PersistentAttribute(Object value) { - this.value = value; - } - - public Object getValue() { - return value; - } + private record PersistentAttribute(Object value) { } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java index e94167c33d..c89ad41e3b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,15 +17,14 @@ package io.cloudbeaver.model.session; -import io.cloudbeaver.DBWConstants; import io.cloudbeaver.DBWUserIdentity; import io.cloudbeaver.DBWebException; import io.cloudbeaver.auth.SMAuthProviderExternal; -import io.cloudbeaver.model.app.WebAuthConfiguration; +import io.cloudbeaver.model.app.ServletAuthConfiguration; import io.cloudbeaver.model.user.WebUser; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.utils.ServletAppUtils; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; @@ -38,7 +37,6 @@ import java.time.OffsetDateTime; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -84,7 +82,7 @@ public List authenticateSession() throws DBException { @SuppressWarnings("unchecked") private List finishWebSessionAuthorization(SMAuthInfo authInfo) throws DBException { - boolean configMode = WebAppUtils.getWebApplication().isConfigurationMode(); + boolean configMode = ServletAppUtils.getServletApplication().isConfigurationMode(); boolean alreadyLoggedIn = webSession.getUser() != null; boolean resetUserStateOnError = !alreadyLoggedIn; @@ -114,15 +112,6 @@ private List finishWebSessionAuthorization(SMAuthInfo authInfo) thr SMAuthProviderExternal authProviderExternal = authProviderInstance instanceof SMAuthProviderExternal ? (SMAuthProviderExternal) authProviderInstance : null; - boolean providerDisabled = !isProviderEnabled(providerId); - if (configMode || webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) { - // 1. Admin can authorize in any providers - // 2. When it authorizes in non-local provider for the first time we force linkUser flag - if (providerDisabled && webSession.getUser() != null) { - linkWithActiveUser = true; - } - } - SMSession authSession; if (authProviderExternal != null && !configMode && !alreadyLoggedIn) { @@ -138,7 +127,7 @@ private List finishWebSessionAuthorization(SMAuthInfo authInfo) thr DBWUserIdentity userIdentity = null; var providerConfigId = authConfiguration.getAuthProviderConfigurationId(); - var providerConfig = WebAppUtils.getWebAuthApplication() + var providerConfig = ServletAppUtils.getAuthApplication() .getAuthConfiguration() .getAuthProviderConfiguration(providerConfigId); if (authProviderExternal != null) { @@ -175,6 +164,7 @@ private List finishWebSessionAuthorization(SMAuthInfo authInfo) thr authProviderDescriptor, userIdentity, authSession, + authInfo, OffsetDateTime.now() ); webAuthInfo.setAuthProviderConfigurationId(authConfiguration.getAuthProviderConfigurationId()); @@ -204,7 +194,8 @@ private WebAuthProviderDescriptor getAuthProvider(String providerId) throws DBWe } private boolean isProviderEnabled(@NotNull String providerId) { - WebAuthConfiguration appConfiguration = (WebAuthConfiguration) WebAppUtils.getWebApplication().getAppConfiguration(); + ServletAuthConfiguration appConfiguration = (ServletAuthConfiguration) ServletAppUtils.getServletApplication() + .getAppConfiguration(); return appConfiguration.isAuthProviderEnabled(providerId); } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java index f0e9c3246b..dab7d1ccb3 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -18,8 +18,8 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.model.websocket.event.WSProjectResourceEvent; import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.model.websocket.event.WSProjectResourceEvent; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @@ -50,6 +50,9 @@ public void setSubscribedProjects(@NotNull Set subscribedProjectIds) { } public boolean isEventAllowed(WSEvent event) { + if (event.isForceProcessed()) { + return true; + } if (!subscribedEventTopics.isEmpty() && !subscribedEventTopics.contains(event.getTopicId()) ) { return false; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionPreferenceStore.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionPreferenceStore.java new file mode 100644 index 0000000000..033e67efaa --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionPreferenceStore.java @@ -0,0 +1,88 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.session; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.impl.preferences.AbstractUserPreferenceStore; +import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; + +import java.io.IOException; +import java.util.Map; + +public class WebSessionPreferenceStore extends AbstractUserPreferenceStore { + + @NotNull + private final WebUserContext userContext; + + public WebSessionPreferenceStore( + @NotNull WebUserContext userContext, + @NotNull DBPPreferenceStore parentStore + ) { + super(parentStore); + this.userContext = userContext; + } + + @NotNull + public Map getCustomUserParameters() { + return userPreferences; + } + + // to avoid redundant sm api call + public void updatePreferenceValues(@NotNull Map newValues) throws DBException { + if (userContext.getUser() != null) { + userContext.getSecurityController().setCurrentUserParameters(newValues); + } + for (Map.Entry entry : newValues.entrySet()) { + if (entry.getValue() == null) { + userPreferences.remove(entry.getKey()); + } else { + userPreferences.put(entry.getKey(), entry.getValue()); + } + } + } + + @Override + protected void setUserPreference(String name, Object value) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getDefaultString(String name) { + return parentStore.getDefaultString(name); + } + + @Override + public boolean isDefault(String name) { + return !userPreferences.containsKey(name) && parentStore.isDefault(name); + } + + @Override + public boolean needsSaving() { + return false; + } + + @Override + public void setToDefault(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void save() throws IOException { + throw new RuntimeException("Not implemented"); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionProvider.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionProvider.java index eeb2e16ccc..7769225bd6 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionProvider.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionProvider.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionWorkspace.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionWorkspace.java index 8e57325443..583a6e7d09 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionWorkspace.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionWorkspace.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,13 +16,13 @@ */ package io.cloudbeaver.model.session; -import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.WebSessionProjectImpl; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.DBPAdaptable; +import org.jkiss.dbeaver.model.DBPImage; import org.jkiss.dbeaver.model.app.DBPPlatform; -import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.app.DBPWorkspace; -import org.jkiss.dbeaver.model.auth.SMSessionContext; import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl; import org.jkiss.dbeaver.model.rm.RMUtils; import org.jkiss.dbeaver.runtime.DBWorkbench; @@ -39,14 +39,18 @@ public class WebSessionWorkspace implements DBPWorkspace { private final BaseWebSession session; private final SessionContextImpl workspaceAuthContext; - private final List accessibleProjects = new ArrayList<>(); - private WebProjectImpl activeProject; + private final List accessibleProjects = new ArrayList<>(); + private WebSessionProjectImpl activeProject; public WebSessionWorkspace(BaseWebSession session) { this.session = session; this.workspaceAuthContext = new SessionContextImpl(null); } + public BaseWebSession getWebSession() { + return session; + } + @NotNull @Override public DBPPlatform getPlatform() { @@ -82,18 +86,20 @@ public Path getMetadataFolder() { @NotNull @Override - public List getProjects() { + public List getProjects() { return accessibleProjects; } + @Nullable @Override - public DBPProject getActiveProject() { + public WebSessionProjectImpl getActiveProject() { return activeProject; } + @Nullable @Override - public WebProjectImpl getProject(@NotNull String projectName) { - for (WebProjectImpl project : accessibleProjects) { + public WebSessionProjectImpl getProject(@NotNull String projectName) { + for (WebSessionProjectImpl project : accessibleProjects) { if (project.getName().equals(projectName)) { return project; } @@ -103,11 +109,11 @@ public WebProjectImpl getProject(@NotNull String projectName) { @Nullable @Override - public WebProjectImpl getProjectById(String projectId) { + public WebSessionProjectImpl getProjectById(@NotNull String projectId) { if (projectId == null) { return activeProject; } - for (WebProjectImpl project : accessibleProjects) { + for (WebSessionProjectImpl project : accessibleProjects) { if (project.getId().equals(projectId)) { return project; } @@ -117,36 +123,41 @@ public WebProjectImpl getProjectById(String projectId) { @NotNull @Override - public SMSessionContext getAuthContext() { + public SessionContextImpl getAuthContext() { return workspaceAuthContext; } @Override - public void dispose() { - clearAuthData(); + public void initializeProjects() { + // noop + } + @Override + public void dispose() { clearProjects(); } - void clearAuthData() { - workspaceAuthContext.close(); + @Nullable + @Override + public DBPImage getResourceIcon(DBPAdaptable resourceAdapter) { + return null; } - public void setActiveProject(DBPProject activeProject) { - this.activeProject = (WebProjectImpl) activeProject; + public void setActiveProject(WebSessionProjectImpl activeProject) { + this.activeProject = activeProject; } - void addProject(WebProjectImpl project) { + void addProject(WebSessionProjectImpl project) { accessibleProjects.add(project); } - void removeProject(WebProjectImpl project) { + void removeProject(WebSessionProjectImpl project) { accessibleProjects.remove(project); } void clearProjects() { if (!this.accessibleProjects.isEmpty()) { - for (WebProjectImpl project : accessibleProjects) { + for (WebSessionProjectImpl project : accessibleProjects) { project.dispose(); } this.activeProject = null; @@ -155,12 +166,12 @@ void clearProjects() { } @Override - public boolean hasRealmPermission(String permission) { + public boolean hasRealmPermission(@NotNull String permission) { return false; } @Override - public boolean supportsRealmFeature(String feature) { + public boolean supportsRealmFeature(@NotNull String feature) { return false; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserAuthToken.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserAuthToken.java new file mode 100644 index 0000000000..4d468f25c8 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserAuthToken.java @@ -0,0 +1,43 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model.session; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; + +import java.time.OffsetDateTime; + + +public interface WebUserAuthToken { + @NotNull + String getAuthProvider(); + + @Nullable + String getAuthConfiguration(); + + @NotNull + OffsetDateTime getLoginTime(); + + @NotNull + String getUserId(); + + @NotNull + String getDisplayName(); + + @Nullable + String getMessage(); +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java index 21725e7ef7..68b7278d82 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,7 +17,7 @@ package io.cloudbeaver.model.session; -import io.cloudbeaver.model.app.WebApplication; +import io.cloudbeaver.model.app.ServletApplication; import io.cloudbeaver.model.user.WebUser; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -36,22 +36,21 @@ import org.jkiss.dbeaver.model.security.SMAdminController; import org.jkiss.dbeaver.model.security.SMController; import org.jkiss.dbeaver.model.security.user.SMAuthPermissions; +import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.CommonUtils; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; /** * Web user context. * Contains user state and services based on available permissions */ +//TODO: split to authenticated and non authenticated context public class WebUserContext implements SMCredentialsProvider { private static final Log log = Log.getLog(WebUserContext.class); - private final WebApplication application; + private final ServletApplication application; private final DBPWorkspace workspace; private WebUser user; @@ -66,13 +65,15 @@ public class WebUserContext implements SMCredentialsProvider { private RMController rmController; private DBFileController fileController; private Set accessibleProjectIds = new HashSet<>(); + private final WebSessionPreferenceStore preferenceStore; - public WebUserContext(WebApplication application, DBPWorkspace workspace) throws DBException { + public WebUserContext(ServletApplication application, DBPWorkspace workspace) throws DBException { this.application = application; this.workspace = workspace; this.securityController = application.createSecurityController(this); this.rmController = application.createResourceController(this, workspace); this.fileController = application.createFileController(this); + this.preferenceStore = new WebSessionPreferenceStore(this, DBWorkbench.getPlatform().getPreferenceStore()); setUserPermissions(getDefaultPermissions()); } @@ -116,7 +117,13 @@ public synchronized boolean refresh( setRefreshToken(smRefreshToken); setUserPermissions(smAuthPermissions.getPermissions()); this.adminSecurityController = application.getAdminSecurityController(this); + this.rmController = application.createResourceController(this, workspace); if (isSessionChanged) { + if (smAuthPermissions.getUserId() != null) { + this.preferenceStore.updateAllUserPreferences(securityController.getCurrentUserParameters()); + } else { + this.preferenceStore.updateAllUserPreferences(Map.of()); + } this.smSessionId = smAuthPermissions.getSessionId(); setUser(smAuthPermissions.getUserId() == null ? null : new WebUser(securityController.getCurrentUser())); refreshAccessibleProjects(); @@ -161,7 +168,7 @@ public synchronized void reset() throws DBException { this.user = null; this.securityController = application.createSecurityController(this); this.adminSecurityController = null; - this.secretController = application.getSecretController(this); + this.secretController = null; } @NotNull @@ -220,7 +227,10 @@ private void setUserPermissions(Set permissions) { this.userPermissions = permissions; } - public DBSSecretController getSecretController() { + public DBSSecretController getSecretController() throws DBException { + if (this.securityController == null) { + this.secretController = application.getSecretController(this, workspace.getAuthContext()); + } return secretController; } @@ -248,4 +258,8 @@ public Set getAccessibleProjectIds() { private void setRefreshToken(@Nullable String refreshToken) { this.refreshToken = refreshToken; } + + public WebSessionPreferenceStore getPreferenceStore() { + return preferenceStore; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/monitor/TaskProgressMonitor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/monitor/TaskProgressMonitor.java new file mode 100644 index 0000000000..d8be4778a7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/monitor/TaskProgressMonitor.java @@ -0,0 +1,55 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.session.monitor; + +import io.cloudbeaver.model.WebAsyncTaskInfo; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.utils.WebEventUtils; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.ProxyProgressMonitor; + +/** + * Task progress monitor. + * Used by async GQL requests. + */ +public class TaskProgressMonitor extends ProxyProgressMonitor { + + @NotNull + private final WebAsyncTaskInfo asyncTask; + private final WebSession webSession; + + public TaskProgressMonitor(DBRProgressMonitor original, @NotNull WebSession webSession, @NotNull WebAsyncTaskInfo asyncTask) { + super(original); + this.webSession = webSession; + this.asyncTask = asyncTask; + } + + @Override + public void beginTask(String name, int totalWork) { + super.beginTask(name, totalWork); + asyncTask.setStatus(name); + WebEventUtils.sendAsyncTaskEvent(webSession, asyncTask); + } + + @Override + public void subTask(String name) { + super.subTask(name); + asyncTask.setStatus(name); + WebEventUtils.sendAsyncTaskEvent(webSession, asyncTask); + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java index f7e151c0d0..4297584009 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,8 +17,10 @@ package io.cloudbeaver.model.user; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.security.user.SMUser; +import java.time.Instant; import java.util.Collections; import java.util.Map; @@ -70,10 +72,15 @@ public Map getConfigurationParameters() { return Collections.emptyMap(); } + @NotNull public String[] getTeams() { return user.getUserTeams(); } + public void setTeams(@NotNull String[] teams) { + user.setUserTeams(teams); + } + @Override public int hashCode() { return user.getUserId().hashCode(); @@ -92,4 +99,19 @@ public String toString() { public String getAuthRole() { return user.getAuthRole(); } + + @Nullable + public Instant getDisableDate() { + return user.getDisableDate(); + } + + @Nullable + public String getDisabledBy() { + return user.getDisabledBy(); + } + + @Nullable + public String getDisableReason() { + return user.getDisableReason(); + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUserOriginInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUserOriginInfo.java index 7b6e275547..06c738f464 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUserOriginInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUserOriginInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -92,7 +92,7 @@ public WebPropertyInfo[] getDetails() throws DBWebException { } try { SMAuthProvider authProvider = this.authProviderDescriptor.getInstance(); - if (authProvider instanceof SMAuthProviderExternal) { + if (authProvider instanceof SMAuthProviderExternal && !authProviderDescriptor.isAuthHidden()) { // read user's info from credentials, previously we tried to read data from external service using // SMAuthProviderExternal#getUserDetails var creds = loadCredentials(); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/utils/ConfigurationUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/utils/ConfigurationUtils.java new file mode 100644 index 0000000000..5ae8ad0e0f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/utils/ConfigurationUtils.java @@ -0,0 +1,44 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.utils; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.DBConstants; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.utils.ArrayUtils; +import org.jkiss.utils.CommonUtils; + +public class ConfigurationUtils { + private ConfigurationUtils() { + } + + public static boolean isDriverEnabled( + @NotNull DBPDriver driver, + @Nullable String[] enabledDrivers, + @Nullable String[] disabledDrivers + ) { + if (ArrayUtils.contains(enabledDrivers, driver.getFullId())) { + return true; + } + if (ArrayUtils.contains(disabledDrivers, driver.getFullId())) { + return false; + } + return !driver.isEmbedded() || CommonUtils.toBoolean(driver.getDriverParameter(DBConstants.PARAM_SAFE_EMBEDDED_DRIVER), false); + } + +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderConfiguration.java index 51e9992606..3b12df2ecb 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderConfiguration.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -18,7 +18,9 @@ import io.cloudbeaver.auth.CBAuthConstants; import io.cloudbeaver.auth.SMAuthProviderFederated; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.auth.SMSignOutLinkProvider; +import io.cloudbeaver.utils.ServletAppUtils; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthProvider; @@ -34,12 +36,21 @@ public class WebAuthProviderConfiguration { private static final Log log = Log.getLog(WebAuthProviderConfiguration.class); + @NotNull private final WebAuthProviderDescriptor providerDescriptor; + @NotNull private final SMAuthProviderCustomConfiguration config; - - public WebAuthProviderConfiguration(WebAuthProviderDescriptor providerDescriptor, SMAuthProviderCustomConfiguration config) { + @NotNull + private final String origin; + + public WebAuthProviderConfiguration( + @NotNull WebAuthProviderDescriptor providerDescriptor, + @NotNull SMAuthProviderCustomConfiguration config, + @NotNull String origin + ) { this.providerDescriptor = providerDescriptor; this.config = config; + this.origin = origin; } public String getProviderId() { @@ -73,33 +84,53 @@ public Map getParameters() { @Property public String getSignInLink() throws DBException { SMAuthProvider instance = providerDescriptor.getInstance(); - return instance instanceof SMAuthProviderFederated ? - buildRedirectUrl(((SMAuthProviderFederated) instance).getSignInLink(getId(), config.getParameters())) + return instance instanceof SMAuthProviderFederated smAuthProviderFederated ? + buildRedirectUrl(smAuthProviderFederated.getSignInLink(getId(), origin), origin) : null; } - private String buildRedirectUrl(String baseUrl) { - return baseUrl + "?" + CBAuthConstants.CB_REDIRECT_URL_REQUEST_PARAM + "=" + WebAppUtils.getWebApplication().getServerURL(); + private String buildRedirectUrl(@NotNull String baseUrl, @NotNull String origin) { + return baseUrl + "?" + CBAuthConstants.CB_REDIRECT_URL_REQUEST_PARAM + "=" + origin; } @Property public String getSignOutLink() throws DBException { SMAuthProvider instance = providerDescriptor.getInstance(); - return instance instanceof SMAuthProviderFederated - ? ((SMAuthProviderFederated) instance).getSignOutLink(getId(), config.getParameters()) + return instance instanceof SMSignOutLinkProvider smSignOutLinkProvider + ? smSignOutLinkProvider.getCommonSignOutLink(getId(), config.getParameters(), origin) : null; } @Property public String getRedirectLink() throws DBException { SMAuthProvider instance = providerDescriptor.getInstance(); - return instance instanceof SMAuthProviderFederated ? ((SMAuthProviderFederated) instance).getRedirectLink(getId(), config.getParameters()) : null; + return instance instanceof SMAuthProviderFederated smAuthProviderFederated + ? smAuthProviderFederated.getRedirectLink(getId(), config.getParameters(), origin) + : null; } @Property public String getMetadataLink() throws DBException { SMAuthProvider instance = providerDescriptor.getInstance(); - return instance instanceof SMAuthProviderFederated ? ((SMAuthProviderFederated) instance).getMetadataLink(getId(), config.getParameters()) : null; + return instance instanceof SMAuthProviderFederated smAuthProviderFederated + ? smAuthProviderFederated.getMetadataLink(getId(), config.getParameters(), origin) + : null; + } + + @Property + public String getAcsLink() throws DBException { + SMAuthProvider instance = providerDescriptor.getInstance(); + return instance instanceof SMAuthProviderFederated smAuthProviderFederated + ? smAuthProviderFederated.getAcsLink(getId(), config.getParameters(), origin) + : null; + } + + @Property + public String getEntityIdLink() throws DBException { + SMAuthProvider instance = providerDescriptor.getInstance(); + return instance instanceof SMAuthProviderFederated smAuthProviderFederated + ? smAuthProviderFederated.getEntityIdLink(getId(), config.getParameters(), origin) + : null; } @Override diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java index 40ef7acbee..6156d3b5de 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,6 +16,7 @@ */ package io.cloudbeaver.registry; +import io.cloudbeaver.auth.SMAuthProviderFederated; import org.eclipse.core.runtime.IConfigurationElement; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -45,40 +46,45 @@ public class WebAuthProviderDescriptor extends AbstractDescriptor { private final IConfigurationElement cfg; - private final ObjectType implType; + private ObjectType implType; private final Map> metaParameters = new HashMap<>(); private SMAuthProvider instance; private final DBPImage icon; - private final Map configurationParameters = new LinkedHashMap<>(); + private final Map configurationParameters = new LinkedHashMap<>(); private final List credentialProfiles = new ArrayList<>(); private final boolean configurable; private final boolean trusted; private final boolean isPrivate; + private final boolean isAuthHidden; + private final boolean isCaseInsensitive; + private final boolean serviceProvider; private final String[] requiredFeatures; + private final boolean isRequired; + private String[] types; public WebAuthProviderDescriptor(IConfigurationElement cfg) { super(cfg); this.cfg = cfg; - this.implType = new ObjectType(cfg, "class"); - this.icon = iconToImage(cfg.getAttribute("icon")); - this.configurable = CommonUtils.toBoolean(cfg.getAttribute("configurable")); - this.trusted = CommonUtils.toBoolean(cfg.getAttribute("trusted")); - this.isPrivate = CommonUtils.toBoolean(cfg.getAttribute("private")); - - for (IConfigurationElement cfgElement : cfg.getChildren("configuration")) { - for (IConfigurationElement propGroup : ArrayUtils.safeArray(cfgElement.getChildren(PropertyDescriptor.TAG_PROPERTY_GROUP))) { - String category = propGroup.getAttribute(PropertyDescriptor.ATTR_LABEL); - IConfigurationElement[] propElements = propGroup.getChildren(PropertyDescriptor.TAG_PROPERTY); - for (IConfigurationElement prop : propElements) { - PropertyDescriptor propertyDescriptor = new PropertyDescriptor(category, prop); - configurationParameters.put(CommonUtils.toString(propertyDescriptor.getId()), propertyDescriptor); - } + this.implType = new ObjectType(cfg, WebRegistryConstant.ATTR_CLASS); + this.icon = iconToImage(cfg.getAttribute(WebRegistryConstant.ATTR_ICON)); + this.configurable = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_CONFIGURABLE)); + this.trusted = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_TRUSTED)); + this.isPrivate = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_PRIVATE)); + this.isRequired = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_REQUIRED)); + this.isAuthHidden = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_AUTH_HIDDEN)); + this.isCaseInsensitive = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_CASE_INSENSITIVE)); + this.serviceProvider = CommonUtils.toBoolean(cfg.getAttribute(WebRegistryConstant.ATTR_SERVICE_PROVIDER)); + + for (IConfigurationElement cfgElement : cfg.getChildren(WebRegistryConstant.TAG_CONFIGURATION)) { + List properties = WebAuthProviderRegistry.readProperties(cfgElement, getId()); + for (WebAuthProviderProperty property : properties) { + configurationParameters.put(CommonUtils.toString(property.getId()), property); } } - for (IConfigurationElement credElement : cfg.getChildren("credentials")) { + for (IConfigurationElement credElement : cfg.getChildren(WebRegistryConstant.TAG_CREDENTIALS)) { credentialProfiles.add(new SMAuthCredentialsProfile(credElement)); } - for (IConfigurationElement mpElement : cfg.getChildren("metaParameters")) { + for (IConfigurationElement mpElement : cfg.getChildren(WebRegistryConstant.TAG_META_PARAMETERS)) { SMSubjectType subjectType = CommonUtils.valueOf(SMSubjectType.class, mpElement.getAttribute("type"), SMSubjectType.user); List metaProps = new ArrayList<>(); for (IConfigurationElement propGroup : ArrayUtils.safeArray(mpElement.getChildren(PropertyDescriptor.TAG_PROPERTY_GROUP))) { @@ -87,25 +93,24 @@ public WebAuthProviderDescriptor(IConfigurationElement cfg) { metaParameters.put(subjectType, metaProps); } - String rfList = cfg.getAttribute("requiredFeatures"); - if (!CommonUtils.isEmpty(rfList)) { - requiredFeatures = rfList.split(","); - } else { - requiredFeatures = null; - } + String rfList = cfg.getAttribute(WebRegistryConstant.ATTR_REQUIRED_FEATURES); + requiredFeatures = CommonUtils.isEmpty(rfList) ? null : rfList.split(","); + + String typesAttr = cfg.getAttribute(WebRegistryConstant.ATTR_CATEGORIES); + this.types = CommonUtils.isEmpty(typesAttr) ? new String[0] : typesAttr.split(","); } @NotNull public String getId() { - return cfg.getAttribute("id"); + return cfg.getAttribute(WebRegistryConstant.ATTR_ID); } public String getLabel() { - return cfg.getAttribute("label"); + return cfg.getAttribute(WebRegistryConstant.ATTR_LABEL); } public String getDescription() { - return cfg.getAttribute("description"); + return cfg.getAttribute(WebRegistryConstant.ATTR_DESCRIPTION); } public DBPImage getIcon() { @@ -124,7 +129,19 @@ public boolean isPrivate() { return isPrivate; } - public List getConfigurationParameters() { + public boolean isRequired() { + return isRequired; + } + + public boolean isAuthHidden() { + return isAuthHidden; + } + + public boolean isCaseInsensitive() { + return isCaseInsensitive; + } + + public List getConfigurationParameters() { return new ArrayList<>(configurationParameters.values()); } @@ -192,4 +209,34 @@ public List getMetaParameters(SMSubjectType subjectType) return metaParameters.get(subjectType); } + public String[] getTypes() { + return types; + } + + public void loadExtraConfig(IConfigurationElement ext) { + //todo read other props if it needs + String typesAttr = ext.getAttribute(WebRegistryConstant.ATTR_CATEGORIES); + this.types = CommonUtils.isEmpty(typesAttr) ? new String[0] : typesAttr.split(","); + this.implType = new ObjectType(ext, WebRegistryConstant.ATTR_CLASS); + for (IConfigurationElement cfgElement : ext.getChildren(WebRegistryConstant.TAG_CONFIGURATION)) { + List properties = WebAuthProviderRegistry.readProperties(cfgElement, getId()); + for (WebAuthProviderProperty property : properties) { + configurationParameters.put(CommonUtils.toString(property.getId()), property); + } + } + replaceContributor(ext.getContributor()); + } + + public boolean isServiceProvider() { + return serviceProvider; + } + + public boolean isFederated() { + try { + implType.checkObjectClass(SMAuthProviderFederated.class); + return true; + } catch (DBException e) { + return false; + } + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderProperty.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderProperty.java new file mode 100644 index 0000000000..b1ceb5502d --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderProperty.java @@ -0,0 +1,74 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.registry; + +import org.eclipse.core.runtime.IConfigurationElement; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.DBPConditionalProperty; +import org.jkiss.dbeaver.model.impl.LocalizedPropertyDescriptor; + +public class WebAuthProviderProperty extends LocalizedPropertyDescriptor implements DBPConditionalProperty { + private final String[] requiredFeatures; + @Nullable + private final String type; + + private final String authProviderId; + private final String hideExpr; + private final String readOnlyExpr; + + public WebAuthProviderProperty(String category, IConfigurationElement config, String authProviderId) { + super(category, config); + this.authProviderId = authProviderId; + String featuresAttr = config.getAttribute("requiredFeatures"); + this.requiredFeatures = featuresAttr == null ? new String[0] : featuresAttr.split(","); + this.type = config.getAttribute("type"); + this.hideExpr = config.getAttribute("hideExpr"); + this.readOnlyExpr = config.getAttribute("readOnlyExpr"); + } + + @NotNull + public String[] getRequiredFeatures() { + return requiredFeatures; + } + + @Nullable + public String getType() { + return type; + } + + @Nullable + @Override + public String getHideExpression() { + return hideExpr; + } + + @Nullable + @Override + public String getReadOnlyExpression() { + return readOnlyExpr; + } + + @Override + public String getPropertyId() { + if (authProviderId != null) { + return "prop.auth.model." + authProviderId + "." + this.getId(); + } else { + return "prop.auth.model." + this.getId(); + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderRegistry.java index e26b0266d7..767b600a16 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderRegistry.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderRegistry.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -20,12 +20,12 @@ import org.eclipse.core.runtime.IExtensionRegistry; import org.eclipse.core.runtime.Platform; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.impl.PropertyDescriptor; import org.jkiss.dbeaver.registry.RegistryConstants; +import org.jkiss.utils.ArrayUtils; +import org.jkiss.utils.CommonUtils; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public class WebAuthProviderRegistry { @@ -33,6 +33,8 @@ public class WebAuthProviderRegistry { private static final String TAG_AUTH_PROVIDER = "authProvider"; //$NON-NLS-1$ private static final String TAG_AUTH_PROVIDER_DISABLE = "authProviderDisable"; //$NON-NLS-1$ + private static final String TAG_COMMON_PROVIDER_PROPERTIES = "commonProviderProperties"; //$NON-NLS-1$ + private static final String ATTRIBUTE_EXTENDED = "extended"; private static WebAuthProviderRegistry instance = null; @@ -45,35 +47,71 @@ public synchronized static WebAuthProviderRegistry getInstance() { } private final Map authProviders = new LinkedHashMap<>(); + private final List commonProperties = new ArrayList<>(); private WebAuthProviderRegistry() { } private void loadExtensions(IExtensionRegistry registry) { - { - IConfigurationElement[] extConfigs = registry.getConfigurationElementsFor(WebAuthProviderDescriptor.EXTENSION_ID); - for (IConfigurationElement ext : extConfigs) { - // Load webServices - if (TAG_AUTH_PROVIDER.equals(ext.getName())) { - WebAuthProviderDescriptor providerDescriptor = new WebAuthProviderDescriptor(ext); + IConfigurationElement[] extConfigs = registry.getConfigurationElementsFor(WebAuthProviderDescriptor.EXTENSION_ID); + // Sort - parse providers with parent in the end + Arrays.sort(extConfigs, (o1, o2) -> { + String p1 = o1.getAttribute(ATTRIBUTE_EXTENDED); + String p2 = o2.getAttribute(ATTRIBUTE_EXTENDED); + if (CommonUtils.equalObjects(p1, p2)) return 0; + if (p1 == null) return -1; + if (p2 == null) return 1; + return 0; + }); + + for (IConfigurationElement ext : extConfigs) { + // Load webServices + if (TAG_AUTH_PROVIDER.equals(ext.getName())) { + WebAuthProviderDescriptor providerDescriptor; + WebAuthProviderDescriptor webAuthProviderDescriptor + = authProviders.get(ext.getAttribute(WebRegistryConstant.ATTR_EXTENDED)); + if (webAuthProviderDescriptor != null) { + webAuthProviderDescriptor.loadExtraConfig(ext); + } else { + providerDescriptor = new WebAuthProviderDescriptor(ext); this.authProviders.put(providerDescriptor.getId(), providerDescriptor); } + } else if (TAG_COMMON_PROVIDER_PROPERTIES.equals(ext.getName())) { + var commonProperties = new WebCommonAuthProviderPropertyDescriptor(ext); + this.commonProperties.add(commonProperties); } + } - for (IConfigurationElement ext : extConfigs) { - // Disable auth providers - if (TAG_AUTH_PROVIDER_DISABLE.equals(ext.getName())) { - String providerId = ext.getAttribute(RegistryConstants.ATTR_ID); - if (!this.authProviders.containsKey(providerId)) { - log.warn("Can't disable auth provider '" + providerId + "' - no such provider found"); - } else { - this.authProviders.remove(providerId); - } + for (IConfigurationElement ext : extConfigs) { + // Disable auth providers + if (TAG_AUTH_PROVIDER_DISABLE.equals(ext.getName())) { + String providerId = ext.getAttribute(RegistryConstants.ATTR_ID); + if (!this.authProviders.containsKey(providerId)) { + log.warn("Can't disable auth provider '" + providerId + "' - no such provider found"); + } else { + this.authProviders.remove(providerId); } } } } + static List readProperties(IConfigurationElement root, String authProviderId) { + List properties = new ArrayList<>(); + for (IConfigurationElement propGroup : ArrayUtils.safeArray(root.getChildren(PropertyDescriptor.TAG_PROPERTY_GROUP))) { + String category = propGroup.getAttribute(PropertyDescriptor.ATTR_LABEL); + IConfigurationElement[] propElements = propGroup.getChildren(PropertyDescriptor.TAG_PROPERTY); + for (IConfigurationElement prop : propElements) { + WebAuthProviderProperty propertyDescriptor = new WebAuthProviderProperty(category, prop, authProviderId); + properties.add(propertyDescriptor); + } + } + return properties; + } + + public List getCommonProperties() { + return commonProperties; + } + public List getAuthProviders() { return new ArrayList<>(authProviders.values()); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebCommonAuthProviderPropertyDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebCommonAuthProviderPropertyDescriptor.java new file mode 100644 index 0000000000..aba6f9af3f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebCommonAuthProviderPropertyDescriptor.java @@ -0,0 +1,74 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.registry; + +import org.eclipse.core.runtime.IConfigurationElement; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.impl.AbstractDescriptor; +import org.jkiss.utils.CommonUtils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class WebCommonAuthProviderPropertyDescriptor extends AbstractDescriptor { + private final Set supportedProviderCategories = new HashSet<>(); + private final Set exclude = new HashSet<>(); + @NotNull + private final List configurationParameters; + + public WebCommonAuthProviderPropertyDescriptor(IConfigurationElement cfg) { + super(cfg); + configurationParameters = WebAuthProviderRegistry.readProperties(cfg, null); + String supportedCategoriesAttr = cfg.getAttribute("supportedProviderCategories"); + if (CommonUtils.isNotEmpty(supportedCategoriesAttr)) { + supportedProviderCategories.addAll(Arrays.stream(supportedCategoriesAttr.split(",")).toList()); + } + + String excludeAttr = cfg.getAttribute("exclude"); + if (CommonUtils.isNotEmpty(excludeAttr)) { + exclude.addAll(Arrays.stream(excludeAttr.split(",")).toList()); + } + } + + + @NotNull + public List getConfigurationParameters() { + return configurationParameters; + } + + private boolean supportAllProviders() { + return supportedProviderCategories.isEmpty() && exclude.isEmpty(); + } + + public boolean isApplicableFor(@NotNull WebAuthProviderDescriptor providerDescriptor) { + if (supportAllProviders()) { + return true; + } + boolean supported = false; + for (String type : providerDescriptor.getTypes()) { + if (exclude.contains(type)) { + return false; + } + if (supportedProviderCategories.contains(type)) { + supported = true; + } + } + return supported; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebDriverRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebDriverRegistry.java new file mode 100644 index 0000000000..a2378c7037 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebDriverRegistry.java @@ -0,0 +1,124 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.registry; + +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtensionRegistry; +import org.eclipse.core.runtime.Platform; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.model.connection.DBPDriverLibrary; +import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; + +public class WebDriverRegistry { + + private static final Log log = Log.getLog(WebDriverRegistry.class); + + private static final String EXTENSION_ID = "io.cloudbeaver.driver"; //$NON-NLS-1$ + private static final String TAG_DRIVER = "driver"; //$NON-NLS-1$ + + private static WebDriverRegistry instance = null; + + public synchronized static WebDriverRegistry getInstance() { + if (instance == null) { + instance = new WebDriverRegistry(); + instance.loadExtensions(Platform.getExtensionRegistry()); + } + return instance; + } + + private final List applicableDrivers = new ArrayList<>(); + private final Set webDrivers = new HashSet<>(); + + protected WebDriverRegistry() { + } + + private void loadExtensions(IExtensionRegistry registry) { + { + IConfigurationElement[] extConfigs = registry.getConfigurationElementsFor(EXTENSION_ID); + for (IConfigurationElement ext : extConfigs) { + // Load webServices + if (TAG_DRIVER.equals(ext.getName())) { + this.webDrivers.add(ext.getAttribute("id")); + } + } + } + } + + public List getApplicableDrivers() { + return applicableDrivers; + } + + /** + * Updates info about applicable drivers (f.e. some changes were made in driver config file). + */ + public void refreshApplicableDrivers() { + this.applicableDrivers.clear(); + this.getSupportedFileOpenExtension().clear(); + this.applicableDrivers.addAll( + DataSourceProviderRegistry.getInstance().getEnabledDataSourceProviders().stream() + .map(DBPDataSourceProviderDescriptor::getEnabledDrivers) + .flatMap(List::stream) + .filter(this::isDriverApplicable) + .peek(this::refreshFileExtensions) + .toList()); + + log.info("Available drivers: " + applicableDrivers.stream().map(DBPDriver::getFullName).collect(Collectors.joining(","))); + } + + @NotNull + public Map> getSupportedFileOpenExtension() { + return new HashMap<>(); + } + + protected void refreshFileExtensions(DBPDriver dbpDriver) { + } + + protected boolean isDriverApplicable(@NotNull DBPDriver driver) { + List libraries = driver.getDriverLibraries(); + if (!webDrivers.contains(driver.getFullId())) { + return false; + } + boolean hasAllFiles = true; + for (DBPDriverLibrary lib : libraries) { + if (!isDriverLibraryFilePresent(lib)) { + hasAllFiles = false; + log.error("\tDriver '" + driver.getId() + "' is missing library '" + lib.getDisplayName() + "'"); + } else { + if (lib.getType() == DBPDriverLibrary.FileType.jar) { + return true; + } + } + } + return hasAllFiles; + } + + private boolean isDriverLibraryFilePresent(@NotNull DBPDriverLibrary lib) { + if (lib.getType() == DBPDriverLibrary.FileType.license) { + return true; + } + Path localFile = lib.getLocalFile(); + return localFile != null && Files.exists(localFile); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebFeatureDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebFeatureDescriptor.java index 675f1bb831..03c014b825 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebFeatureDescriptor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebFeatureDescriptor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -18,11 +18,12 @@ package io.cloudbeaver.registry; import io.cloudbeaver.DBWFeatureSet; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.utils.ServletAppUtils; import org.eclipse.core.runtime.IConfigurationElement; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.DBPImage; import org.jkiss.dbeaver.model.impl.AbstractContextDescriptor; +import org.jkiss.utils.CommonUtils; /** * WebFeatureDescriptor @@ -35,6 +36,7 @@ public class WebFeatureDescriptor extends AbstractContextDescriptor implements D private final String label; private final String description; private final DBPImage icon; + private final boolean enabledByDefault; public WebFeatureDescriptor(IConfigurationElement config) { @@ -43,6 +45,7 @@ public WebFeatureDescriptor(IConfigurationElement config) this.label = config.getAttribute("label"); this.description = config.getAttribute("description"); this.icon = iconToImage(config.getAttribute("icon")); + this.enabledByDefault = CommonUtils.getBoolean(config.getAttribute("enabledByDefault"), false); } @NotNull @@ -66,7 +69,12 @@ public DBPImage getIcon() { @Override public boolean isEnabled() { - return WebAppUtils.getWebApplication().getAppConfiguration().isFeatureEnabled(this.id); + return ServletAppUtils.getServletApplication().getAppConfiguration().isFeatureEnabled(this.id); + } + + @Override + public boolean isEnabledByDefault() { + return enabledByDefault; } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebFeatureRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebFeatureRegistry.java index 8c0c35c135..47ed10d7df 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebFeatureRegistry.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebFeatureRegistry.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebMetaParametersRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebMetaParametersRegistry.java index 7bacaa0853..3ed7ef2e58 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebMetaParametersRegistry.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebMetaParametersRegistry.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebPermissionDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebPermissionDescriptor.java index 5d7cc14e27..7403560fc1 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebPermissionDescriptor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebPermissionDescriptor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebRegistryConstant.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebRegistryConstant.java new file mode 100644 index 0000000000..31c06abb49 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebRegistryConstant.java @@ -0,0 +1,39 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.registry; + +public class WebRegistryConstant { + public static final String ATTR_ID = "id"; + public static final String ATTR_EXTENDED = "extended"; + public static final String ATTR_LABEL = "label"; + public static final String ATTR_DESCRIPTION = "description"; + public static final String ATTR_CLASS = "class"; + public static final String ATTR_ICON = "icon"; + public static final String ATTR_CONFIGURABLE = "configurable"; + public static final String ATTR_TRUSTED = "trusted"; + public static final String ATTR_PRIVATE = "private"; + public static final String ATTR_REQUIRED = "required"; + public static final String ATTR_AUTH_HIDDEN = "authHidden"; + public static final String ATTR_CASE_INSENSITIVE = "caseInsensitive"; + public static final String ATTR_REQUIRED_FEATURES = "requiredFeatures"; + public static final String ATTR_CATEGORIES = "categories"; + public static final String ATTR_SERVICE_PROVIDER = "serviceProvider"; + + public static final String TAG_CONFIGURATION = "configuration"; + public static final String TAG_CREDENTIALS = "credentials"; + public static final String TAG_META_PARAMETERS = "metaParameters"; +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureDescriptor.java new file mode 100644 index 0000000000..d89bf19429 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureDescriptor.java @@ -0,0 +1,76 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.registry; + +import io.cloudbeaver.DBWFeatureSet; +import org.eclipse.core.runtime.IConfigurationElement; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.DBPImage; +import org.jkiss.dbeaver.model.impl.AbstractContextDescriptor; + +/** + * WebFeatureDescriptor + */ +public class WebServerFeatureDescriptor extends AbstractContextDescriptor implements DBWFeatureSet { + + public static final String EXTENSION_ID = "io.cloudbeaver.server.feature"; //$NON-NLS-1$ + + private final String id; + private final String label; + private final String description; + private final DBPImage icon; + + public WebServerFeatureDescriptor(IConfigurationElement config) + { + super(config); + this.id = config.getAttribute("id"); + this.label = config.getAttribute("label"); + this.description = config.getAttribute("description"); + this.icon = iconToImage(config.getAttribute("icon")); + } + + @NotNull + public String getId() { + return id; + } + + @NotNull + public String getLabel() { + return label; + } + + public String getDescription() { + return description; + } + + @Override + public DBPImage getIcon() { + return icon; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean isEnabledByDefault() { + return false; + } + +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureRegistry.java new file mode 100644 index 0000000000..32a8c7a59c --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureRegistry.java @@ -0,0 +1,68 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.registry; + +import io.cloudbeaver.DBWFeatureSet; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtensionRegistry; +import org.eclipse.core.runtime.Platform; +import org.jkiss.dbeaver.Log; + +import java.util.ArrayList; +import java.util.List; + +public class WebServerFeatureRegistry { + + private static final Log log = Log.getLog(WebServerFeatureRegistry.class); + + private static final String TAG_FEATURE = "feature"; //$NON-NLS-1$ + + private static WebServerFeatureRegistry instance = null; + + public synchronized static WebServerFeatureRegistry getInstance() { + if (instance == null) { + instance = new WebServerFeatureRegistry(); + instance.loadExtensions(Platform.getExtensionRegistry()); + } + return instance; + } + + private String[] serverFeatures = new String[0]; + + private WebServerFeatureRegistry() { + } + + private synchronized void loadExtensions(IExtensionRegistry registry) { + IConfigurationElement[] extConfigs = registry.getConfigurationElementsFor(WebServerFeatureDescriptor.EXTENSION_ID); + List features = new ArrayList<>(); + for (IConfigurationElement ext : extConfigs) { + if (TAG_FEATURE.equals(ext.getName())) { + features.add( + new WebServerFeatureDescriptor(ext)); + } + } + this.serverFeatures = features + .stream() + .map(DBWFeatureSet::getId) + .toArray(String[]::new); + } + + public String[] getServerFeatures() { + return serverFeatures; + } + +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServiceDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServiceDescriptor.java index a433cc9157..99fb5c1792 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServiceDescriptor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServiceDescriptor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServiceRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServiceRegistry.java index 1ddc2ad510..73bbdf274e 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServiceRegistry.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServiceRegistry.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebValueSerializerDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebValueSerializerDescriptor.java index 7d39ac53e7..334ad5216e 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebValueSerializerDescriptor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebValueSerializerDescriptor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseServletPlatform.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseServletPlatform.java new file mode 100644 index 0000000000..49523ee106 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseServletPlatform.java @@ -0,0 +1,84 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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; + +import io.cloudbeaver.DBWConstants; +import io.cloudbeaver.model.app.ServletApplication; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.registry.BasePlatformImpl; +import org.jkiss.dbeaver.utils.ContentUtils; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.StandardConstants; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public abstract class BaseServletPlatform extends BasePlatformImpl { + private static final Log log = Log.getLog(BaseServletPlatform.class); + public static final String BASE_TEMP_DIR = "dbeaver"; + + protected volatile Path tempFolder; + + @NotNull + public Path getTempFolder(@NotNull DBRProgressMonitor monitor, @NotNull String name) { + if (tempFolder == null) { + synchronized (this) { + if (tempFolder == null) { + initTempFolder(monitor); + } + } + } + Path folder = tempFolder.resolve(name); + if (!Files.exists(folder)) { + try { + Files.createDirectories(folder); + } catch (IOException e) { + log.error("Error creating temp folder '" + folder.toAbsolutePath() + "'", e); + } + } + return folder; + } + + public abstract ServletApplication getApplication(); + + private void initTempFolder(@NotNull DBRProgressMonitor monitor) { + // Make temp folder + monitor.subTask("Create temp folder"); + String sysTempFolder = System.getProperty(StandardConstants.ENV_TMP_DIR); + if (CommonUtils.isNotEmpty(sysTempFolder)) { + tempFolder = Path.of(sysTempFolder).resolve(BASE_TEMP_DIR).resolve(DBWConstants.WORK_DATA_FOLDER_NAME); + } else { + //we do not use workspace because it can be in external file system + tempFolder = getApplication().getHomeDirectory().resolve(DBWConstants.WORK_DATA_FOLDER_NAME); + } + } + + public synchronized void dispose() { + // Remove temp folder + if (tempFolder != null) { + + if (!ContentUtils.deleteFileRecursive(tempFolder.toFile())) { + log.warn("Can't delete temp folder '" + tempFolder.toAbsolutePath() + "'"); + } + tempFolder = null; + } + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/CBConstants.java similarity index 79% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/CBConstants.java index 3364ae53df..d1ff7817fb 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/CBConstants.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -20,25 +20,37 @@ * Various constants */ public class CBConstants { + public static final int STATIC_CACHE_SECONDS = 60 * 60 * 24 * 3; + + public static final String CONF_DIR_NAME = "conf"; public static final String RUNTIME_DATA_DIR_NAME = ".data"; public static final String RUNTIME_APP_CONFIG_FILE_NAME = ".cloudbeaver.runtime.conf"; public static final String RUNTIME_PRODUCT_CONFIG_FILE_NAME = ".product.runtime.conf"; public static final String AUTO_CONFIG_FILE_NAME = ".cloudbeaver.auto.conf"; + public static final String PARAM_SERVER_CONFIGURATION = "server"; public static final String PARAM_SERVER_PORT = "serverPort"; public static final String PARAM_SERVER_HOST = "serverHost"; public static final String PARAM_SERVER_NAME = "serverName"; + public static final String PARAM_FORCE_HTTPS = "forceHttps"; + public static final String PARAM_BIND_SESSION_TO_IP = "bindSessionToIp"; + public static final String PARAM_SUPPORTED_HOSTS = "supportedHosts"; + public static final String PARAM_SSL_CONFIGURATION_PATH = "sslConfigurationPath"; public static final String PARAM_CONTENT_ROOT = "contentRoot"; public static final String PARAM_SERVER_URL = "serverURL"; public static final String PARAM_ROOT_URI = "rootURI"; public static final String PARAM_SERVICES_URI = "serviceURI"; public static final String PARAM_DRIVERS_LOCATION = "driversLocation"; public static final String PARAM_WORKSPACE_LOCATION = "workspaceLocation"; + //legacy path to a product.conf file + @Deprecated public static final String PARAM_PRODUCT_CONFIGURATION = "productConfiguration"; + public static final String PARAM_PRODUCT_SETTINGS = "productSettings"; public static final String PARAM_EXTERNAL_PROPERTIES = "externalProperties"; public static final String PARAM_STATIC_CONTENT = "staticContent"; public static final String PARAM_RESOURCE_QUOTAS = "resourceQuotas"; public static final String PARAM_RESOURCE_MANAGER_ENABLED = "resourceManagerEnabled"; + public static final String PARAM_SECRET_MANAGER_ENABLED = "secretManagerEnabled"; public static final String PARAM_SHOW_READ_ONLY_CONN_INFO = "showReadOnlyConnectionInfo"; public static final String PARAM_CONN_GRANT_ANON_ACCESS = "grantConnectionsAccessToAnonymousTeam"; public static final String PARAM_AUTH_PROVIDERS = "authConfiguration"; @@ -49,6 +61,7 @@ public class CBConstants { public static final String PARAM_DEVEL_MODE = "develMode"; public static final String PARAM_SECURITY_MANAGER = "enableSecurityManager"; public static final String PARAM_SM_CONFIGURATION = "sm"; + public static final String PARAM_PASSWORD_POLICY_CONFIGURATION = "passwordPolicy"; public static final int DEFAULT_SERVER_PORT = 8080; //public static final String DEFAULT_SERVER_NAME = "CloudBeaver Web Server"; @@ -79,6 +92,10 @@ public class CBConstants { public static final String APPLICATION_JSON = "application/json"; - public static final String QUOTA_PROP_FILE_LIMIT = "dataExportFileSizeLimit"; public static final String ADMIN_AUTO_GRANT = "auto-grant"; + public static final String HOST_LOCALHOST = "localhost"; + public static final String HOST_127_0_0_1 = "127.0.0.1"; + + public static final String BIND_SESSION_ENABLE = "enable"; + public static final String BIND_SESSION_DISABLE = "disable"; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/HttpConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/HttpConstants.java new file mode 100644 index 0000000000..f2170a477d --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/HttpConstants.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +public interface HttpConstants { + String CONTENT_TYPE = "Content-Type"; + + String TYPE_JSON = "application/json"; + String HEADER_AUTHORIZATION = "Authorization"; +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/ServerGlobalWorkspace.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/ServerGlobalWorkspace.java new file mode 100644 index 0000000000..4f99ca0a0e --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/ServerGlobalWorkspace.java @@ -0,0 +1,109 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.model.app.ServletApplication; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.app.DBPPlatform; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl; +import org.jkiss.dbeaver.model.impl.app.BaseWorkspaceImpl; +import org.jkiss.utils.CommonUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Web global workspace. + */ +public class ServerGlobalWorkspace extends BaseWorkspaceImpl { + + private static final Log log = Log.getLog(ServerGlobalWorkspace.class); + + protected final Map projects = new LinkedHashMap<>(); + private WebGlobalProject globalProject; + + private final ServletApplication application; + + public ServerGlobalWorkspace( + @NotNull DBPPlatform platform, + @NotNull ServletApplication application + ) { + super(platform, application.getWorkspaceDirectory()); + this.application = application; + } + + @Override + public void initializeProjects() { + initializeWorkspaceSession(); + + // Load global project + String defaultProjectName = application.getDefaultProjectName(); + if (CommonUtils.isNotEmpty(defaultProjectName)) { + Path globalProjectPath = getAbsolutePath().resolve(defaultProjectName); + if (!Files.exists(globalProjectPath)) { + try { + Files.createDirectories(globalProjectPath); + } catch (IOException e) { + log.error("Error creating global project path: " + globalProject, e); + } + } + } + + globalProject = new WebGlobalProject( + this, + getAuthContext(), + CommonUtils.notEmpty(defaultProjectName) + ); + activeProject = globalProject; + } + + @NotNull + @Override + public String getWorkspaceId() { + return readWorkspaceIdProperty(); + } + + @Nullable + @Override + public DBPProject getActiveProject() { + return super.getActiveProject(); + } + + @NotNull + @Override + public List getProjects() { + return Collections.singletonList(globalProject); + } + + @Nullable + @Override + public BaseProjectImpl getProject(@NotNull String projectName) { + if (globalProject != null && globalProject.getId().equals(projectName)) { + return globalProject; + } + return null; + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalProject.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalProject.java new file mode 100644 index 0000000000..2999947646 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalProject.java @@ -0,0 +1,85 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.dbeaver.model.auth.SMSessionContext; +import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl; +import org.jkiss.dbeaver.registry.DataSourceRegistry; + +import java.nio.file.Path; + +/** + * Web global project. + */ +public class WebGlobalProject extends BaseProjectImpl { + + private static final Log log = Log.getLog(WebGlobalProject.class); + + private final String projectName; + + public WebGlobalProject( + @NotNull DBPWorkspace workspace, + @Nullable SMSessionContext sessionContext, + @NotNull String projectName + ) { + super(workspace, sessionContext); + this.projectName = projectName; + } + + @Override + public boolean isVirtual() { + return false; + } + + @NotNull + @Override + public String getName() { + return projectName; + } + + @NotNull + @Override + public Path getAbsolutePath() { + return getWorkspace().getAbsolutePath().resolve(projectName); + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public void ensureOpen() { + + } + + @Override + public boolean isUseSecretStorage() { + return false; + } + + @NotNull + @Override + protected DBPDataSourceRegistry createDataSourceRegistry() { + return new DataSourceRegistry<>(this); + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java deleted file mode 100644 index 3def57f3f3..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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; - -import org.eclipse.core.resources.IWorkspace; -import org.jkiss.dbeaver.model.app.DBPPlatform; -import org.jkiss.dbeaver.registry.EclipseWorkspaceImpl; - -/** - * Web global workspace. - *

- * Basically just a wrapper around Eclipse workspace. - */ -public class WebGlobalWorkspace extends EclipseWorkspaceImpl { - - public WebGlobalWorkspace(DBPPlatform platform, IWorkspace eclipseWorkspace) { - super(platform, eclipseWorkspace); - } - -} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java index 342e9c8f9b..d13eb03bcf 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,8 +16,6 @@ */ package io.cloudbeaver.server; -import org.eclipse.core.resources.IWorkspace; -import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Plugin; import org.jkiss.dbeaver.ModelPreferences; import org.jkiss.dbeaver.model.impl.preferences.BundlePreferenceStore; @@ -25,7 +23,6 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; -import java.io.File; import java.io.PrintStream; /** @@ -35,7 +32,6 @@ public class WebPlatformActivator extends Plugin { // The shared instance private static WebPlatformActivator instance; - private static File configDir; private PrintStream debugWriter; private DBPPreferenceStore preferences; @@ -76,13 +72,6 @@ public DBPPreferenceStore getPreferences() { return preferences; } - /** - * Returns the workspace instance. - */ - public static IWorkspace getWorkspace() { - return ResourcesPlugin.getWorkspace(); - } - protected void shutdownPlatform() { } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java new file mode 100644 index 0000000000..f1e3bf5c66 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java @@ -0,0 +1,191 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import io.cloudbeaver.utils.ServletAppUtils; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.impl.preferences.AbstractPreferenceStore; +import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; + +import java.io.IOException; +import java.util.Map; + +public class WebServerPreferenceStore extends AbstractPreferenceStore { + private final DBPPreferenceStore parentStore; + + public WebServerPreferenceStore( + @NotNull DBPPreferenceStore parentStore + ) { + this.parentStore = parentStore; + } + + @Override + public boolean contains(String name) { + return productConf().containsKey(name) || parentStore.contains(name); + } + + @Override + public boolean getBoolean(String name) { + return toBoolean(getString(name)); + } + + @Override + public double getDouble(String name) { + return toDouble(getString(name)); + } + + @Override + public float getFloat(String name) { + return toFloat(getString(name)); + } + + @Override + public int getInt(String name) { + return toInt(getString(name)); + } + + @Override + public long getLong(String name) { + return toLong(getString(name)); + } + + @Override + public String getString(String name) { + Object value = productConf().get(name); + if (value == null) { + return parentStore.getString(name); + } + return value.toString(); + } + + @Override + public boolean getDefaultBoolean(String name) { + return getBoolean(name); + } + + @Override + public double getDefaultDouble(String name) { + return getDouble(name); + } + + @Override + public float getDefaultFloat(String name) { + return getFloat(name); + } + + @Override + public int getDefaultInt(String name) { + return getInt(name); + } + + @Override + public long getDefaultLong(String name) { + return getLong(name); + } + + @Override + public String getDefaultString(String name) { + // TODO: split product.conf and runtime.product.conf + return getString(name); + } + + @Override + public boolean isDefault(String name) { + return true; + } + + @Override + public boolean needsSaving() { + return false; + } + + @Override + public void setDefault(String name, double value) { + setDefault(name, String.valueOf(value)); + } + + @Override + public void setDefault(String name, float value) { + setDefault(name, String.valueOf(value)); + } + + @Override + public void setDefault(String name, int value) { + setDefault(name, String.valueOf(value)); + } + + @Override + public void setDefault(String name, long value) { + setDefault(name, String.valueOf(value)); + } + + @Override + public void setDefault(String name, String defaultObject) { + // do not store global default properties in product.conf + this.parentStore.setDefault(name, defaultObject); + } + + @Override + public void setDefault(String name, boolean value) { + setDefault(name, String.valueOf(value)); + } + + @Override + public void setToDefault(String name) { + parentStore.setToDefault(name); + } + + @Override + public void setValue(String name, double value) { + parentStore.setValue(name, value); + } + + @Override + public void setValue(String name, float value) { + parentStore.setValue(name, value); + } + + @Override + public void setValue(String name, int value) { + parentStore.setValue(name, value); + } + + @Override + public void setValue(String name, long value) { + parentStore.setValue(name, value); + } + + @Override + public void setValue(String name, String value) { + parentStore.setValue(name, value); + } + + @Override + public void setValue(String name, boolean value) { + parentStore.setValue(name, value); + } + + @Override + public void save() throws IOException { + throw new RuntimeException("Not Implemented"); + } + + private Map productConf() { + var app = ServletAppUtils.getServletApplication(); + return app.getServerConfiguration().getProductSettings(); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/filters/ServerConfigurationTimeLimitFilter.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/filters/ServerConfigurationTimeLimitFilter.java new file mode 100644 index 0000000000..d669e8ae32 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/filters/ServerConfigurationTimeLimitFilter.java @@ -0,0 +1,64 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.filters; + +import graphql.execution.instrumentation.InstrumentationContext; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimpleInstrumentationContext; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; +import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; +import graphql.validation.ValidationError; +import io.cloudbeaver.model.app.ServletApplication; +import org.jkiss.dbeaver.Log; + +import java.time.Duration; +import java.util.List; + +public class ServerConfigurationTimeLimitFilter extends SimplePerformantInstrumentation { + private static final Log log = Log.getLog(ServerConfigurationTimeLimitFilter.class); + + private static final int MINUTES_OF_INACTION_BEFORE_DISABLING_REQUEST_PROCESSING = 60; + private final ServletApplication application; + + public ServerConfigurationTimeLimitFilter(ServletApplication application) { + this.application = application; + } + + @Override + public InstrumentationContext> beginValidation( + InstrumentationValidationParameters parameters, + InstrumentationState state + ) { + boolean isOutOfTime = System.currentTimeMillis() - application.getApplicationStartTime() + > Duration.ofMinutes(MINUTES_OF_INACTION_BEFORE_DISABLING_REQUEST_PROCESSING).toMillis(); + if (application.isConfigurationMode() && isOutOfTime) { + log.warn("Server configuration time has expired. A server restart is required to continue."); + ValidationError error = ValidationError.newValidationError() + .description("Server configuration time has expired. A server restart is required to continue.") + .build(); + return new SimpleInstrumentationContext<>() { + @Override + public void onCompleted(List result, Throwable t) { + result.clear(); + result.add(error); + } + }; + } + return super.beginValidation(parameters, state); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWService.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWService.java index cc20faaa24..5650c96dd5 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWService.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWService.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBinding.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBinding.java index 54e420140b..70cd6641a5 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBinding.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBinding.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBindingServlet.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBindingServlet.java index e6f5b014ba..580316e7bf 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBindingServlet.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBindingServlet.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,13 +16,24 @@ */ package io.cloudbeaver.service; -import io.cloudbeaver.model.app.WebApplication; +import io.cloudbeaver.model.app.ServletApplication; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; +import java.util.List; + /** * Servlet service */ -public interface DBWServiceBindingServlet extends DBWServiceBinding { +public interface DBWServiceBindingServlet extends DBWServiceBinding { + default boolean isApplicable(ServletApplication application) { + return true; + } void addServlets(APPLICATION application, DBWServletContext servletContext) throws DBException; + + @NotNull + default List getExcludedServletPaths(@NotNull APPLICATION application) { + return List.of(); + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceServerConfigurator.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceServerConfigurator.java index b0f3e632ba..dd28cde931 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceServerConfigurator.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceServerConfigurator.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,12 +16,13 @@ */ package io.cloudbeaver.service; -import io.cloudbeaver.model.app.WebAppConfiguration; +import io.cloudbeaver.model.app.ServletAppConfiguration; +import io.cloudbeaver.model.app.ServletApplication; +import io.cloudbeaver.model.app.ServletServerConfiguration; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.app.DBPApplication; /** * Web service implementation @@ -29,11 +30,16 @@ public interface DBWServiceServerConfigurator extends DBWServiceBinding { void configureServer( - @NotNull DBPApplication application, + @NotNull ServletApplication application, @Nullable WebSession session, - @NotNull WebAppConfiguration appConfig + @NotNull ServletServerConfiguration serverConfiguration, + @NotNull ServletAppConfiguration appConfig ) throws DBException; - void reloadConfiguration(@NotNull WebAppConfiguration appConfig) throws DBException; + default void migrateConfigurationIfNeeded(@NotNull ServletApplication application) throws DBException { + + } + + void reloadConfiguration(@NotNull ServletAppConfiguration appConfig) throws DBException; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServletContext.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServletContext.java index a4b3fc15c1..48c351f1f2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServletContext.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServletContext.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -17,10 +17,9 @@ package io.cloudbeaver.service; +import jakarta.servlet.http.HttpServlet; import org.jkiss.dbeaver.DBException; -import javax.servlet.http.HttpServlet; - public interface DBWServletContext { void addServlet(String servletId, HttpServlet servlet, String mapping) throws DBException; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWSessionHandler.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWSessionHandler.java index 95cd5f7fb8..03e1543a48 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWSessionHandler.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWSessionHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,10 +17,10 @@ package io.cloudbeaver.service; import io.cloudbeaver.model.session.WebSession; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jkiss.dbeaver.DBException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/admin/AdminPermissionInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/admin/AdminPermissionInfo.java index b96e48d6c0..06d8b66f19 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/admin/AdminPermissionInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/admin/AdminPermissionInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/security/SMUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/security/SMUtils.java index 508d3f61be..b08b09b9c5 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/security/SMUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/security/SMUtils.java @@ -1,3 +1,19 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.security; import io.cloudbeaver.DBWConstants; @@ -10,6 +26,7 @@ import org.jkiss.dbeaver.model.rm.RMConstants; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.rm.RMProjectPermission; +import org.jkiss.dbeaver.model.security.exception.SMAccessTokenExpiredException; import org.jkiss.dbeaver.model.security.exception.SMRefreshTokenExpiredException; import java.util.ArrayList; @@ -55,7 +72,7 @@ public static List findPermissions(@NotNull String permissi return permissionInfos; } - public static boolean isTokenExpiredExceptionWasHandled(Throwable ex) { + public static boolean isRefreshTokenExpiredExceptionWasHandled(Throwable ex) { if (ex instanceof SMRefreshTokenExpiredException) { return true; } @@ -69,4 +86,17 @@ public static boolean isTokenExpiredExceptionWasHandled(Throwable ex) { return false; } + public static boolean isAccessTokenExpiredExceptionWasHandled(Throwable ex) { + if (ex instanceof SMAccessTokenExpiredException) { + return true; + } + + Throwable cause = ex; + while ((cause = cause.getCause()) != null) { + if (cause instanceof SMAccessTokenExpiredException) { + return true; + } + } + return false; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/DBWValueSerializer.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/DBWValueSerializer.java index b29f528d1a..e52e54a01d 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/DBWValueSerializer.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/DBWValueSerializer.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebDataFormat.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebDataFormat.java index b08fce004d..c93354edc7 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebDataFormat.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebDataFormat.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java index cc9c2819b1..561162394c 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -22,7 +22,6 @@ public class WebSQLConstants { public static final String QUOTA_PROP_ROW_LIMIT = "sqlResultSetRowsLimit"; - public static final String QUOTA_PROP_MEMORY_LIMIT = "sqlResultSetMemoryLimit"; public static final String QUOTA_PROP_QUERY_LIMIT = "sqlMaxRunningQueries"; public static final String QUOTA_PROP_SQL_QUERY_TIMEOUT = "sqlQueryTimeout"; public static final String QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH = "sqlTextPreviewMaxLength"; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLDataFilterConstraint.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLDataFilterConstraint.java index ab3bfcca1a..ff23f8e143 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLDataFilterConstraint.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLDataFilterConstraint.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java index 3fd68da3c2..7d46135da7 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -22,9 +22,8 @@ import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.data.DBDAttributeBinding; import org.jkiss.dbeaver.model.data.DBDRowIdentifier; -import org.jkiss.dbeaver.model.struct.DBSAttributeBase; -import org.jkiss.dbeaver.model.struct.DBSDataContainer; -import org.jkiss.dbeaver.model.struct.DBSTypedObject; +import org.jkiss.dbeaver.model.exec.trace.DBCTrace; +import org.jkiss.dbeaver.model.struct.*; import java.util.HashSet; import java.util.List; @@ -40,6 +39,9 @@ public class WebSQLResultsInfo { @NotNull private final String id; private DBDAttributeBinding[] attributes; + // TODO: find a way to remove isSingleRow and use virtual keys for reading BLOB and string cell values. + private boolean isSingleRow; + private DBCTrace trace; private String queryText; public WebSQLResultsInfo(@NotNull DBSDataContainer dataContainer, @NotNull String id) { @@ -126,4 +128,27 @@ public DBSTypedObject getAttributeByPosition(int pos) { return null; } + public boolean canRefreshResults() { + DBSEntity entity = getDefaultRowIdentifier().getEntity(); + // FIXME: do not refresh documents for now. Can be solved by extracting document ID attributes + // FIXME: but it will require to provide dynamic document metadata. + return entity == null || entity.getDataSource() == null || + (!(entity instanceof DBSDocumentContainer) && !entity.getDataSource().getInfo().isDynamicMetadata()); + } + + public DBCTrace getTrace() { + return trace; + } + + public void setTrace(@NotNull DBCTrace trace) { + this.trace = trace; + } + + public boolean isSingleRow() { + return isSingleRow; + } + + public void setSingleRow(boolean singleRow) { + isSingleRow = singleRow; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/test/WebGQLClient.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/test/WebGQLClient.java new file mode 100644 index 0000000000..93d0ab37ca --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/test/WebGQLClient.java @@ -0,0 +1,124 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.test; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.utils.GeneralUtils; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * GraphQL client for tests. + */ +public class WebGQLClient { + private static final Gson gson = new GsonBuilder() + .setPrettyPrinting() + .create(); + public static final String GQL_AUTHENTICATE = """ + query authLogin($provider: ID!, $configuration: ID, $credentials: Object, $linkUser: Boolean, $forceSessionsLogout: Boolean) { + result: authLogin( + provider: $provider + configuration: $configuration + credentials: $credentials + linkUser: $linkUser + forceSessionsLogout: $forceSessionsLogout + ) { + authId + authStatus + } + }"""; + + @NotNull + private final HttpClient httpClient; + @NotNull + private final String apiUrl; + + public WebGQLClient(@NotNull HttpClient httpClient, @NotNull String apiUrl) { + this.httpClient = httpClient; + this.apiUrl = apiUrl; + } + + /** + * Sends GraphQL request without additional headers + * + * @param query GraphQL query + * @param variables GraphQL query variables + * @return GraphQL response + */ + @NotNull + public T sendQuery(@NotNull String query, @Nullable Map variables) throws Exception { + return sendQueryWithHeaders(query, variables, List.of()); + } + + /** + * Sends GraphQL request without additional headers + * + * @param query GraphQL query + * @param variables GraphQL query variables + * @param headers HTTP request headers + * @return GraphQL response + */ + @NotNull + public T sendQueryWithHeaders( + @NotNull String query, + @Nullable Map variables, + @NotNull List headers + ) throws Exception { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(URI.create(apiUrl)) + .POST(HttpRequest.BodyPublishers.ofString(makeGQLRequest(query, variables))) + .setHeader("TE-Client-Version", GeneralUtils.getMajorVersion()) + .header("Content-Type", "application/json"); + if (!headers.isEmpty()) { + requestBuilder.headers(headers.toArray(String[]::new)); + } + HttpRequest request = requestBuilder.build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + Map body = gson.fromJson( + response.body(), + JSONUtils.MAP_TYPE_TOKEN + ); + if (body.containsKey("errors")) { + String message = JSONUtils.getString(JSONUtils.getObjectList(body, "errors").get(0), "message"); + throw new DBException(message); + } + // graphql response will be in "data" key + return (T) JSONUtils.getObject(body, "data").get("result"); + } + + @NotNull + private String makeGQLRequest(@NotNull String text, @Nullable Map variables) { + Map request = new LinkedHashMap<>(); + request.put("query", text); + if (variables != null && !variables.isEmpty()) { + request.put("variables", variables); + } + + return gson.toJson(request, Map.class); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/CBModelConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/CBModelConstants.java index d76e34655e..bbe2ab8096 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/CBModelConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/CBModelConstants.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/ServletAppUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/ServletAppUtils.java new file mode 100644 index 0000000000..8ebcbf0333 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/ServletAppUtils.java @@ -0,0 +1,374 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.utils; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.auth.NoAuthCredentialsProvider; +import io.cloudbeaver.model.app.ServletApplication; +import io.cloudbeaver.model.app.ServletAuthApplication; +import io.cloudbeaver.model.session.WebSession; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.auth.SMAuthenticationManager; +import org.jkiss.dbeaver.model.rm.RMProjectType; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.utils.CommonUtils; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.*; + +public class ServletAppUtils { + private static final String HEADER_ORIGIN = "Origin"; + private static final String HEADER_X_ORIGIN = "X-Origin"; + private static final String HEADER_REFERER = "Referer"; + + private static final String HEADER_FORWARDED_SCHEME = "X-Forwarded-Scheme"; + private static final String HEADER_FORWARDED_HOST = "X-Forwarded-Host"; + private static final Set DEFAULT_PORTS = Set.of( + 80, + 443 + ); + + + private static final Log log = Log.getLog(ServletAppUtils.class); + + public static String getRelativePath(String path, String curDir) { + return getRelativePath(path, Path.of(curDir)); + } + + public static String getRelativePath(String path, Path curDir) { + if (path.startsWith("/") || path.length() > 2 && path.charAt(1) == ':') { + return path; + } + return curDir.resolve(path).toAbsolutePath().toString(); + } + + public static ServletApplication getServletApplication() { + return (ServletApplication) DBWorkbench.getPlatform().getApplication(); + } + + public static ServletAuthApplication getAuthApplication() throws DBException { + ServletApplication application = getServletApplication(); + if (!ServletAuthApplication.class.isAssignableFrom(application.getClass())) { + throw new DBException("The current application doesn't contain authorization configuration"); + } + return (ServletAuthApplication) application; + } + + public static SMAuthenticationManager getAuthManager(ServletApplication application) throws DBException { + var smController = application.createSecurityController(new NoAuthCredentialsProvider()); + if (!SMAuthenticationManager.class.isAssignableFrom(smController.getClass())) { + throw new DBException("The current application cannot be used for authorization"); + } + return (SMAuthenticationManager) smController; + } + + @SuppressWarnings("unchecked") + public static Map mergeConfigurations( + Map priorityConfiguration, + Map additional + ) { + var resultConfig = new HashMap(); + Set rootKeys = new HashSet<>(priorityConfiguration.keySet()); + rootKeys.addAll(additional.keySet()); + + for (var rootKey : rootKeys) { + var originValue = priorityConfiguration.get(rootKey); + var additionalValue = additional.get(rootKey); + + if (originValue == null || additionalValue == null) { + if (additional.containsKey(rootKey)) { + if (additionalValue != null) { + resultConfig.put(rootKey, additionalValue); + } + } else if (originValue != null) { + resultConfig.put(rootKey, originValue); + } + continue; + } + + if (originValue instanceof Map) { + var resultValue = mergeConfigurations((Map) originValue, (Map) additionalValue); + resultConfig.put(rootKey, resultValue); + } else { + resultConfig.put(rootKey, additionalValue); + } + + } + + return resultConfig; + } + + @SuppressWarnings("unchecked") + public static Map mergeConfigurationsWithVariables(Map origin, Map additional) { + var resultConfig = new HashMap(); + Set rootKeys = new HashSet<>(additional.keySet()); + + for (var rootKey : rootKeys) { + var originValue = origin.get(rootKey); + var additionalValue = additional.get(rootKey); + + if (additionalValue == null) { + continue; + } + + if (originValue instanceof Map) { + var resultValue = mergeConfigurationsWithVariables((Map) originValue, (Map) additionalValue); + resultConfig.put(rootKey, resultValue); + } else { + resultConfig.put(rootKey, getExtractedValue(originValue, additionalValue)); + } + + } + + return resultConfig; + } + + public static Object getExtractedValue(Object oldValue, Object newValue) { + if (!(oldValue instanceof String)) { + return newValue; + } + //new value already contains variable pattern + if (newValue instanceof String newStringValue && GeneralUtils.isVariablePattern(newStringValue)) { + return newValue; + } + String value = (String) oldValue; + if (!GeneralUtils.isVariablePattern(value)) { + return newValue; + } + String extractedVariable = GeneralUtils.extractVariableName(value); + if (extractedVariable != null) { + return GeneralUtils.variablePattern(extractedVariable + ":" + newValue); + } else { + return newValue; + } + } + + + @NotNull + public static String removeSideSlashes(String action) { + if (CommonUtils.isEmpty(action)) { + return action; + } + while (action.startsWith("/")) action = action.substring(1); + while (action.endsWith("/")) action = action.substring(0, action.length() - 1); + return action; + } + + @NotNull + public static StringBuilder getAuthApiUri(@NotNull String serviceId, @NotNull String origin) throws DBException { + return getAuthApiUri(getAuthApplication(), serviceId, origin); + } + + @NotNull + public static StringBuilder getAuthApiUri( + @NotNull ServletAuthApplication webAuthApplication, + @NotNull String serviceId, + @NotNull String origin + ) throws DBException { + String finalOrigin = webAuthApplication.modifyOrigin(origin); + StringBuilder authUriBuilder = new StringBuilder(removeSideSlashes(finalOrigin)); + String serviceUriSegment = removeSideSlashes(webAuthApplication.getAuthServiceUriSegment()); + if (CommonUtils.isNotEmpty(serviceUriSegment)) { + authUriBuilder.append("/").append(serviceUriSegment); + } + authUriBuilder.append("/").append(serviceId).append("/"); + return authUriBuilder; + } + + public static void addResponseCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, long maxSessionIdleTime) { + addResponseCookie(request, response, cookieName, cookieValue, maxSessionIdleTime, null); + } + + public static void addResponseCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, long maxSessionIdleTime, @Nullable String sameSite) { + Cookie sessionCookie = new Cookie(cookieName, cookieValue); + if (maxSessionIdleTime > 0) { + sessionCookie.setMaxAge((int) (maxSessionIdleTime / 1000)); + } + + String path = getServletApplication().getServerConfiguration().getRootURI(); + + if (sameSite != null) { + if (!request.isSecure()) { + log.debug("Attempt to set Cookie `" + cookieName + "` with `SameSite=" + sameSite + "` failed, it " + + "require a secure context/HTTPS"); + } else { + sessionCookie.setSecure(true); + path = path.concat("; SameSite=" + sameSite); + } + } + sessionCookie.setHttpOnly(true); + sessionCookie.setPath(path); + response.addCookie(sessionCookie); + } + + public static String getRequestCookie(HttpServletRequest request, String cookieName) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(cookieName)) { + return cookie.getValue(); + } + } + } + return null; + } + + public static boolean isGlobalProject(DBPProject project) { + return project.getId().equals(getGlobalProjectId()); + } + + public static String getGlobalProjectId() { + String globalConfigurationName = getServletApplication().getDefaultProjectName(); + return RMProjectType.GLOBAL.getPrefix() + "_" + globalConfigurationName; + } + + public static WebProjectImpl getProjectById(WebSession webSession, String projectId) throws DBWebException { + WebProjectImpl project = webSession.getProjectById(projectId); + if (project == null) { + throw new DBWebException("Project '" + projectId + "' not found"); + } + return project; + } + + public static Map flattenMap(Map nestedMap) { + Map result = new LinkedHashMap<>(); + flattenMapHelper(nestedMap, result, ""); + return result; + } + + private static void flattenMapHelper(Map nestedMap, Map result, String prefix) { + for (Map.Entry entry : nestedMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + flattenResult(result, prefix, key, value); + } + } + + private static void flattenResult(Map result, String prefix, String key, Object value) { + if (value instanceof Map) { + flattenMapHelper((Map) value, result, prefix + key + "."); + } else if (value instanceof Object[]) { + flattenArray((Object[]) value, result, prefix + key + "."); + } else { + String fullKey = prefix + key; + if (!result.containsKey(fullKey)) { + result.put(fullKey, value); + } + } + } + + private static void flattenArray(Object[] array, Map result, String prefix) { + for (int i = 0; i < array.length; i++) { + String key = String.valueOf(i); + Object value = array[i]; + + flattenResult(result, prefix, key, value); + } + } + + @NotNull + public static String getOriginFromRequest(@NotNull HttpServletRequest request) { + String origin = request.getHeader(HEADER_ORIGIN); + if (CommonUtils.isEmpty(origin)) { + origin = request.getHeader(HEADER_X_ORIGIN); + } + if (CommonUtils.isEmpty(origin)) { + origin = request.getHeader(HEADER_REFERER); + } + String forwardedScheme = request.getHeader(HEADER_FORWARDED_SCHEME); + String forwardedHost = request.getHeader(HEADER_FORWARDED_HOST); + if (CommonUtils.isNotEmpty(forwardedScheme) && CommonUtils.isNotEmpty(forwardedHost)) { + origin = forwardedScheme + "://" + forwardedHost; + } + if (CommonUtils.isEmpty(origin)) { + URI requestUrl = URI.create(request.getRequestURL().toString()); + origin = getRootUrlFromUri(requestUrl) + "/"; + } + origin = removeSideSlashes(origin); + var app = ServletAppUtils.getServletApplication(); + String rootUri = removeSideSlashes(app.getRootURI()); + if (!origin.endsWith(rootUri)) { + origin = origin + "/" + rootUri + "/"; + } + + origin = removeSideSlashes(origin); + URI uri = URI.create(origin); + if (DEFAULT_PORTS.contains(uri.getPort())) { + try { + origin = new URI( + uri.getScheme(), + uri.getUserInfo(), + uri.getHost(), + -1, + uri.getPath(), + uri.getQuery(), + uri.getFragment() + ).toString(); + } catch (URISyntaxException e) { + log.error("Failed to create URI without port", e); + } + } + + return origin; + } + + public static String getRootUrlFromUri(@NotNull URI uri) { + var builder = new StringBuilder() + .append(uri.getScheme()) + .append("://") + .append(uri.getHost()); + if (uri.getPort() > 0) { + builder.append(":").append(uri.getPort()); + } + return removeSideSlashes(substringBeforeRootURI(builder.toString())); + } + + public static String substringBeforeRootURI(@NotNull String uri) { + String rootUri = removeSideSlashes(getServletApplication().getRootURI()); + if (CommonUtils.isEmpty(rootUri)) { + return uri; + } + rootUri = "/" + rootUri; + if (!uri.endsWith(rootUri)) { + rootUri = rootUri + "/"; + } + return substringBeforeLast(uri, rootUri); + } + + public static String substringBeforeLast( + @NotNull String string, + @NotNull String separator + ) { + if (CommonUtils.isNotEmpty(string) && CommonUtils.isNotEmpty(separator)) { + int pos = string.lastIndexOf(separator); + return pos == -1 ? string : string.substring(0, pos); + } else { + return string; + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java deleted file mode 100644 index d076318f3e..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * 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.utils; - -import io.cloudbeaver.auth.NoAuthCredentialsProvider; -import io.cloudbeaver.model.app.WebApplication; -import io.cloudbeaver.model.app.WebAuthApplication; -import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.app.DBPProject; -import org.jkiss.dbeaver.model.auth.SMAuthenticationManager; -import org.jkiss.dbeaver.model.rm.RMProjectType; -import org.jkiss.dbeaver.runtime.DBWorkbench; -import org.jkiss.dbeaver.utils.GeneralUtils; -import org.jkiss.utils.CommonUtils; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.nio.file.Path; -import java.util.*; - -public class WebAppUtils { - private static final Log log = Log.getLog(WebAppUtils.class); - - public static String getRelativePath(String path, String curDir) { - return getRelativePath(path, Path.of(curDir)); - } - - public static String getRelativePath(String path, Path curDir) { - if (path.startsWith("/") || path.length() > 2 && path.charAt(1) == ':') { - return path; - } - return curDir.resolve(path).toAbsolutePath().toString(); - } - - public static WebApplication getWebApplication() { - return (WebApplication) DBWorkbench.getPlatform().getApplication(); - } - - public static WebAuthApplication getWebAuthApplication() throws DBException { - WebApplication application = getWebApplication(); - if (!WebAuthApplication.class.isAssignableFrom(application.getClass())) { - throw new DBException("The current application doesn't contain authorization configuration"); - } - return (WebAuthApplication) application; - } - - public static SMAuthenticationManager getAuthManager(WebApplication application) throws DBException { - var smController = application.createSecurityController(new NoAuthCredentialsProvider()); - if (!SMAuthenticationManager.class.isAssignableFrom(smController.getClass())) { - throw new DBException("The current application cannot be used for authorization"); - } - return (SMAuthenticationManager) smController; - } - - @SuppressWarnings("unchecked") - public static Map mergeConfigurations(Map origin, Map additional) { - var resultConfig = new HashMap(); - Set rootKeys = new HashSet<>(origin.keySet()); - rootKeys.addAll(additional.keySet()); - - for (var rootKey : rootKeys) { - var originValue = origin.get(rootKey); - var additionalValue = additional.get(rootKey); - - if (originValue == null || additionalValue == null) { - var resultValue = originValue != null ? originValue : additionalValue; - resultConfig.put(rootKey, resultValue); - continue; - } - - if (originValue instanceof Map) { - var resultValue = mergeConfigurations((Map) originValue, (Map) additionalValue); - resultConfig.put(rootKey, resultValue); - } else { - resultConfig.put(rootKey, additionalValue); - } - - } - - return resultConfig; - } - - @SuppressWarnings("unchecked") - public static Map mergeConfigurationsWithVariables(Map origin, Map additional) { - var resultConfig = new HashMap(); - Set rootKeys = new HashSet<>(additional.keySet()); - - for (var rootKey : rootKeys) { - var originValue = origin.get(rootKey); - var additionalValue = additional.get(rootKey); - - if (additionalValue == null) { - continue; - } - - if (originValue instanceof Map) { - var resultValue = mergeConfigurationsWithVariables((Map) originValue, (Map) additionalValue); - resultConfig.put(rootKey, resultValue); - } else { - resultConfig.put(rootKey, getExtractedValue(originValue, additionalValue)); - } - - } - - return resultConfig; - } - - public static Object getExtractedValue(Object oldValue, Object newValue) { - if (!(oldValue instanceof String)) { - return newValue; - } - String value = (String) oldValue; - if (!GeneralUtils.isVariablePattern(value)) { - return newValue; - } - String extractedVariable = GeneralUtils.extractVariableName(value); - if (extractedVariable != null) { - return GeneralUtils.variablePattern(extractedVariable + ":" + newValue); - } else { - return newValue; - } - } - - - @NotNull - public static String removeSideSlashes(String action) { - if (CommonUtils.isEmpty(action)) { - return action; - } - while (action.startsWith("/")) action = action.substring(1); - while (action.endsWith("/")) action = action.substring(0, action.length() - 1); - return action; - } - - @NotNull - public static StringBuilder getAuthApiPrefix(String serviceId) throws DBException { - return getAuthApiPrefix(getWebAuthApplication(), serviceId); - } - - @NotNull - public static StringBuilder getAuthApiPrefix(WebAuthApplication webAuthApplication, String serviceId) { - String authUrl = removeSideSlashes(webAuthApplication.getAuthServiceURL()); - StringBuilder apiPrefix = new StringBuilder(authUrl); - apiPrefix.append("/").append(serviceId).append("/"); - return apiPrefix; - } - - public static void addResponseCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, long maxSessionIdleTime) { - addResponseCookie(request, response, cookieName, cookieValue, maxSessionIdleTime, null); - } - - public static void addResponseCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, long maxSessionIdleTime, @Nullable String sameSite) { - Cookie sessionCookie = new Cookie(cookieName, cookieValue); - if (maxSessionIdleTime > 0) { - sessionCookie.setMaxAge((int) (maxSessionIdleTime / 1000)); - } - - String path = getWebApplication().getRootURI(); - - if (sameSite != null) { - if (sameSite.toLowerCase() == "none" && request.isSecure() == false) { - log.debug("Attempt to set Cookie `" + cookieName + "` with `SameSite=None` failed, it require a secure context/HTTPS"); - } else { - sessionCookie.setSecure(true); - path = path.concat("; SameSite=" + sameSite); - } - } - - sessionCookie.setPath(path); - response.addCookie(sessionCookie); - } - - public static String getRequestCookie(HttpServletRequest request, String cookieName) { - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals(cookieName)) { - return cookie.getValue(); - } - } - } - return null; - } - - public static boolean isGlobalProject(DBPProject project) { - return project.getId().equals(getGlobalProjectId()); - } - - public static String getGlobalProjectId() { - String globalConfigurationName = getWebApplication().getDefaultProjectName(); - return RMProjectType.GLOBAL.getPrefix() + "_" + globalConfigurationName; - } - -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebCommonUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebCommonUtils.java index ae2c5f834b..1a819bbea2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebCommonUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebCommonUtils.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -43,7 +43,7 @@ public static WebPropertyInfo[] getObjectProperties(WebSession session, DBPObjec PropertyCollector propertyCollector = new PropertyCollector(details, false); propertyCollector.collectProperties(); return Arrays.stream(propertyCollector.getProperties()) - .filter(p -> !(p instanceof ObjectPropertyDescriptor && ((ObjectPropertyDescriptor) p).isHidden())) + .filter(p -> !(p instanceof ObjectPropertyDescriptor objProp && objProp.isHidden())) .map(p -> new WebPropertyInfo(session, p, propertyCollector)).toArray(WebPropertyInfo[]::new); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java index 1013b91543..bcd6ff7ce8 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -21,11 +21,6 @@ import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.DBPDataSourceFolder; -import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; -import org.jkiss.dbeaver.model.navigator.DBNLocalFolder; -import org.jkiss.dbeaver.model.navigator.DBNNode; -import org.jkiss.dbeaver.model.navigator.DBNProject; -import org.jkiss.dbeaver.model.navigator.DBNRoot; import org.jkiss.utils.CommonUtils; public class WebConnectionFolderUtils { @@ -49,19 +44,4 @@ public static void validateConnectionFolder(String folderName) throws DBWebExcep throw new DBWebException("Folder name '" + folderName + "' contains illegal characters: /"); } } - - public static DBPDataSourceFolder getParentFolder(DBNNode folderNode) throws DBWebException { - if (folderNode instanceof DBNRoot || folderNode instanceof DBNProject) { - return null; - } else if (folderNode instanceof DBNLocalFolder) { - return ((DBNLocalFolder) folderNode).getFolder(); - } else { - throw new DBWebException("Navigator node '" + folderNode.getNodeItemPath() + "' is not a folder node"); - } - } - - public static DBPDataSourceFolder createFolder(WebConnectionFolderInfo parentFolder, String newName, DBPDataSourceRegistry registry) throws DBWebException { - DBPDataSourceFolder folder = registry.addFolder(parentFolder == null ? null : parentFolder.getDataSourceFolder(), newName); - return folder; - } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebDataSourceUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebDataSourceUtils.java index 41174d4395..23717d53a1 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebDataSourceUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebDataSourceUtils.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,27 +16,35 @@ */ package io.cloudbeaver.utils; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import com.google.gson.Strictness; import io.cloudbeaver.DBWConstants; import io.cloudbeaver.DBWebException; +import io.cloudbeaver.WebSessionProjectImpl; import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.WebNetworkHandlerConfigInput; -import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.access.DBAAuthCredentials; +import org.jkiss.dbeaver.model.access.DBAAuthCredentialsWithComplexProperties; import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration; import org.jkiss.dbeaver.model.net.ssh.SSHConstants; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceDisconnectEvent; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.CommonUtils; import java.util.List; import java.util.Map; +import java.util.Optional; public class WebDataSourceUtils { @@ -97,6 +105,10 @@ public static void updateHandlerConfig(DBWHandlerConfiguration handlerConfig, We private static void setSecureProperties(DBWHandlerConfiguration handlerConfig, WebNetworkHandlerConfigInput cfgInput, boolean ignoreNulls) { var secureProperties = cfgInput.getSecureProperties(); if (secureProperties == null) { + if (!handlerConfig.isSavePassword()) { + // clear all secure properties from handler config + handlerConfig.setSecureProperties(Map.of()); + } return; } for (var pr : secureProperties.entrySet()) { @@ -109,14 +121,22 @@ private static void setSecureProperties(DBWHandlerConfiguration handlerConfig, W @Nullable public static DBPDataSourceContainer getLocalOrGlobalDataSource( - WebApplication application, WebSession webSession, @Nullable String projectId, String connectionId + WebSession webSession, @Nullable String projectId, String connectionId ) throws DBWebException { DBPDataSourceContainer dataSource = null; if (!CommonUtils.isEmpty(connectionId)) { - dataSource = webSession.getProjectById(projectId).getDataSourceRegistry().getDataSource(connectionId); - if (dataSource == null && (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN) || application.isConfigurationMode())) { + WebSessionProjectImpl project = webSession.getProjectById(projectId); + if (project == null) { + throw new DBWebException("Project '" + projectId + "' not found"); + } + dataSource = project.getDataSourceRegistry().getDataSource(connectionId); + if (dataSource == null && + (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN) || webSession.getApplication().isConfigurationMode())) { // If called for new connection in admin mode then this connection may absent in session registry yet - dataSource = getGlobalDataSourceRegistry().getDataSource(connectionId); + project = webSession.getGlobalProject(); + if (project != null) { + dataSource = project.getDataSourceRegistry().getDataSource(connectionId); + } } } return dataSource; @@ -144,6 +164,14 @@ public static boolean disconnectDataSource(@NotNull WebSession webSession, @NotN if (dataSource.isConnected()) { try { dataSource.disconnect(webSession.getProgressMonitor()); + webSession.addSessionEvent( + new WSDataSourceDisconnectEvent( + dataSource.getProject().getId(), + dataSource.getId(), + webSession.getSessionId(), + webSession.getUserId() + ) + ); return true; } catch (DBException e) { log.error("Error closing connection", e); @@ -153,4 +181,42 @@ public static boolean disconnectDataSource(@NotNull WebSession webSession, @NotN } return false; } + + /** + * The method that seeks for web connection in session cache by connection id. + * Mostly used when project id is not defined. + */ + @NotNull + public static WebConnectionInfo getWebConnectionInfo( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull String connectionId + ) throws DBWebException { + if (projectId == null) { + webSession.addWarningMessage("Project id is not defined in request. Try to find it from connection cache"); + // try to find connection in all accessible projects + Optional optional = webSession.getAccessibleProjects().stream() + .flatMap(p -> p.getConnections().stream()) // get connection cache from web projects + .filter(e -> e.getId().contains(connectionId)) + .findFirst(); + if (optional.isPresent()) { + return optional.get(); + } + } + return webSession.getAccessibleProjectById(projectId).getWebConnectionInfo(connectionId); + } + + + public static void updateCredentialsFromProperties(@NotNull DBAAuthCredentials credentials, @NotNull Map properties) { + InstanceCreator credTypeAdapter = type -> credentials; + Gson credGson = new GsonBuilder() + .setStrictness(Strictness.LENIENT) + .registerTypeAdapter(credentials.getClass(), credTypeAdapter) + .create(); + + if (credentials instanceof DBAAuthCredentialsWithComplexProperties complexProperties) { + complexProperties.updateCredentialsFromComplexProperties(properties); + } + credGson.fromJson(credGson.toJsonTree(properties), credentials.getClass()); + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebEventUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebEventUtils.java index 63141a4386..a524ee011c 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebEventUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebEventUtils.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,16 +16,21 @@ */ package io.cloudbeaver.utils; +import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.app.DBPProject; -import org.jkiss.dbeaver.model.rm.RMResource; +import org.jkiss.dbeaver.model.security.SMSubjectType; import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.model.websocket.event.WSEvent; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDatasourceFolderEvent; +import org.jkiss.dbeaver.model.websocket.event.permissions.WSSubjectPermissionEvent; import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceProperty; import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceUpdatedEvent; +import org.jkiss.dbeaver.model.websocket.event.session.WSSessionTaskInfoEvent; import java.util.List; @@ -81,7 +86,7 @@ public static void addDataSourceUpdatedEvent( if (event == null) { return; } - WebAppUtils.getWebApplication().getEventController().addEvent(event); + ServletAppUtils.getServletApplication().getEventController().addEvent(event); } public static void addNavigatorNodeUpdatedEvent( @@ -123,7 +128,7 @@ public static void addNavigatorNodeUpdatedEvent( if (event == null) { return; } - WebAppUtils.getWebApplication().getEventController().addEvent(event); + ServletAppUtils.getServletApplication().getEventController().addEvent(event); } public static void addRmResourceUpdatedEvent( @@ -180,7 +185,32 @@ public static void addRmResourceUpdatedEvent( if (event == null) { return; } - WebAppUtils.getWebApplication().getEventController().addEvent(event); + ServletAppUtils.getServletApplication().getEventController().addEvent(event); + } + + public static void sendAsyncTaskEvent(@NotNull WebSession webSession, @NotNull WebAsyncTaskInfo taskInfo) { + webSession.addSessionEvent( + new WSSessionTaskInfoEvent( + taskInfo.getId(), + taskInfo.getStatus(), + taskInfo.isRunning() + ) + ); + } + + public static void addSubjectPermissionsUpdateEvent( + @NotNull String subjectId, + @NotNull SMSubjectType subjectType, + @Nullable String smSessionId, + @Nullable String userId + ) { + var event = WSSubjectPermissionEvent.update( + smSessionId, + userId, + subjectType, + subjectId + ); + ServletAppUtils.getServletApplication().getEventController().addEvent(event); } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebTestUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebTestUtils.java index 3bb5282a28..b4c131f815 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebTestUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebTestUtils.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,63 +17,19 @@ package io.cloudbeaver.utils; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; -import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.utils.GeneralUtils; -import java.io.File; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Map; public class WebTestUtils { - public static final String GQL_TEMPLATE_AUTH_LOGIN = "authLogin.json"; - - public static String readScriptTemplate(String templateName, Path scriptsPath) throws Exception { - Path templatePath = new File(String.valueOf(scriptsPath), templateName).toPath(); - return Files.readString(templatePath); - } - - - public static Map doPost(String apiUrl, String input, HttpClient client) throws Exception { - return doPostWithHeaders(apiUrl, input, client, List.of()); - } - - public static Map doPostWithHeaders( - String apiUrl, - String input, - HttpClient client, - List headers - ) throws Exception { - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(URI.create(apiUrl)) - .POST(HttpRequest.BodyPublishers.ofString(input)) - .header("Content-Type", "application/json"); - - if (!headers.isEmpty()) { - requestBuilder.headers(headers.toArray(String[]::new)); - } - HttpRequest request = requestBuilder.build(); - HttpResponse response = client.send(request, - HttpResponse.BodyHandlers.ofString()); - - return new GsonBuilder().create().fromJson( - response.body(), - new TypeToken>() { - }.getType() - ); - } - public static boolean getServerStatus(HttpClient client, String apiUrl) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(apiUrl)) + .setHeader("TE-Client-Version", GeneralUtils.getMajorVersion()) .GET() .build(); try { @@ -83,14 +39,4 @@ public static boolean getServerStatus(HttpClient client, String apiUrl) { return false; } } - - public static Map authenticateUser(HttpClient client, Path scriptsPath, String apiUrl) throws Exception { - String input = WebTestUtils.readScriptTemplate(GQL_TEMPLATE_AUTH_LOGIN, scriptsPath); - Map map = WebTestUtils.doPost(apiUrl, input, client); - Map data = JSONUtils.getObjectOrNull(map, "data"); - if (data != null) { - return JSONUtils.getObjectOrNull(data, "authInfo"); - } - return Collections.emptyMap(); - } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/file/UniversalFileVisitor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/file/UniversalFileVisitor.java index 5a9b6dbd7b..45cc942831 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/file/UniversalFileVisitor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/file/UniversalFileVisitor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. 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..c5a8f95796 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -23,4 +23,5 @@ public interface CBWebSessionEventHandler { void handleWebSessionEvent(WSEvent event) throws DBException; void close(); + } diff --git a/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF index c7e9b59cd4..4471474f7e 100644 --- a/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF @@ -3,10 +3,9 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Community Product Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true -Bundle-Version: 23.2.2.qualifier -Bundle-Release-Date: 20230209 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 25.2.1.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy -Bundle-ClassPath: . -Require-Bundle: io.cloudbeaver.server +Require-Bundle: io.cloudbeaver.server.ce Automatic-Module-Name: io.cloudbeaver.product.ce diff --git a/server/bundles/io.cloudbeaver.product.ce/pom.xml b/server/bundles/io.cloudbeaver.product.ce/pom.xml index c4857af4e5..4c3e641860 100644 --- a/server/bundles/io.cloudbeaver.product.ce/pom.xml +++ b/server/bundles/io.cloudbeaver.product.ce/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.product.ce - 23.2.2-SNAPSHOT + 25.2.1-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.product.ce/src/io/cloudbeaver/server/CBServerCE.java b/server/bundles/io.cloudbeaver.product.ce/src/io/cloudbeaver/server/CBServerCE.java index b61c8b0dde..d88e5b7a07 100644 --- a/server/bundles/io.cloudbeaver.product.ce/src/io/cloudbeaver/server/CBServerCE.java +++ b/server/bundles/io.cloudbeaver.product.ce/src/io/cloudbeaver/server/CBServerCE.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF index 709d9344d9..bcb6410426 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF @@ -1,10 +1,9 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 -Bundle-Name: Base JDBC drivers +Bundle-Name: Cloudbeaver Base JDBC drivers Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true -Bundle-Version: 1.0.83.qualifier -Bundle-Release-Date: 20230209 +Bundle-Version: 1.0.130.qualifier +Bundle-Release-Date: 20250922 Bundle-Vendor: DBeaver Corp Bundle-ActivationPolicy: lazy Automatic-Module-Name: io.cloudbeaver.resources.drivers.base -Require-Bundle: io.cloudbeaver.server diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/plugin.xml b/server/bundles/io.cloudbeaver.resources.drivers.base/plugin.xml index 32a81ec9f7..47e6ec4e8c 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/plugin.xml +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/plugin.xml @@ -4,24 +4,29 @@ + - + + + + + @@ -29,13 +34,16 @@ - + + + + @@ -49,12 +57,17 @@ - + + + + + + diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml index 687af1ea85..15cf808ddc 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml @@ -9,6 +9,6 @@ ../ io.cloudbeaver.resources.drivers.base - 1.0.83-SNAPSHOT + 1.0.130-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.server.ce/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.server.ce/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..3b0a1f9f72 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/META-INF/MANIFEST.MF @@ -0,0 +1,21 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Vendor: DBeaver Corp +Bundle-Name: Cloudbeaver CE Server +Bundle-SymbolicName: io.cloudbeaver.server.ce;singleton:=true +Bundle-Version: 25.2.1.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 +Bundle-ActivationPolicy: lazy +Bundle-Activator: io.cloudbeaver.server.CBPlatformActivator +Require-Bundle: io.cloudbeaver.server;visibility:=reexport, + com.google.guava +Export-Package: io.cloudbeaver, + io.cloudbeaver.model, + io.cloudbeaver.model.config, + io.cloudbeaver.server, + io.cloudbeaver.service, + io.cloudbeaver.service.core, + io.cloudbeaver.service.session +Import-Package: org.slf4j +Automatic-Module-Name: io.cloudbeaver.server.ce diff --git a/server/bundles/io.cloudbeaver.server.ce/build.properties b/server/bundles/io.cloudbeaver.server.ce/build.properties new file mode 100644 index 0000000000..742a80a09d --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/build.properties @@ -0,0 +1,6 @@ +source..=src/ +output..=target/classes/ +bin.includes=.,\ + META-INF/,\ + schema/,\ + plugin.xml diff --git a/server/bundles/io.cloudbeaver.server.ce/plugin.xml b/server/bundles/io.cloudbeaver.server.ce/plugin.xml new file mode 100644 index 0000000000..4c3287e279 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/plugin.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/bundles/io.cloudbeaver.server.ce/pom.xml b/server/bundles/io.cloudbeaver.server.ce/pom.xml new file mode 100644 index 0000000000..f237be27a5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + io.cloudbeaver + bundles + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.server.ce + 25.2.1-SNAPSHOT + eclipse-plugin + + diff --git a/server/bundles/io.cloudbeaver.server.ce/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server.ce/schema/service.core.graphqls new file mode 100644 index 0000000000..56c9ab1f68 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/schema/service.core.graphqls @@ -0,0 +1,22 @@ +extend type ServerConfig { + serverURL: String! @deprecated + rootURI: String! + hostName: String! @deprecated(reason: "use container id instead") + containerId: String! + defaultAuthRole: String + defaultUserTeam: String # [23.2.2] + + sessionExpireTime: Int! + localHostAddress: String + + redirectOnFederatedAuth: Boolean! + enabledAuthProviders: [ID!]! + passwordPolicyConfiguration: PasswordPolicyConfig! @since(version: "23.3.3") + "Mark all cookies as secure, recommend for HTTPS deployment configurations" + forceHttps: Boolean! @since(version: "25.1.3") + "List of allowed hosts to use the application - if empty, all hosts are allowed" + supportedHosts: [String!]! @since(version: "25.1.3") + "Indicates whether the session should be linked to the user's IP address" + bindSessionToIp: String! @since(version: "25.1.4") +} + diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/CBWebServerConfig.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/CBWebServerConfig.java new file mode 100644 index 0000000000..38e43a25c8 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/CBWebServerConfig.java @@ -0,0 +1,108 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model; + +import io.cloudbeaver.model.config.PasswordPolicyConfiguration; +import io.cloudbeaver.server.CBApplication; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.utils.CommonUtils; + +import java.util.List; + +public class CBWebServerConfig extends WebServerConfig { + private final CBApplication cbApp; + + public CBWebServerConfig(@NotNull CBApplication cbApp) { + super(cbApp); + this.cbApp = cbApp; + } + + @Property + public String getServerURL() { + return CommonUtils.notEmpty(cbApp.getServerConfiguration().getServerURL()); + } + + @Property + public String getRootURI() { + return CommonUtils.notEmpty(cbApp.getServerConfiguration().getRootURI()); + } + + @Deprecated + @Property + public String getHostName() { + return getContainerId(); + } + + @Property + public String getContainerId() { + return CommonUtils.notEmpty(cbApp.getContainerId()); + } + + @Property + public boolean isRedirectOnFederatedAuth() { + return cbApp.getAppConfiguration().isRedirectOnFederatedAuth(); + } + + @Property + public String getLocalHostAddress() { + return cbApp.getLocalHostAddress(); + } + + @Property + public long getSessionExpireTime() { + return cbApp.getServerConfiguration().getMaxSessionIdleTime(); + } + + @Property + public String[] getEnabledAuthProviders() { + return cbApp.getAppConfiguration().getEnabledAuthProviders(); + } + + @Property + public String getDefaultAuthRole() { + return cbApp.getDefaultAuthRole(); + } + + + @Property + public String getDefaultUserTeam() { + return cbApp.getAppConfiguration().getDefaultUserTeam(); + } + + @Property + public PasswordPolicyConfiguration getPasswordPolicyConfiguration() { + return cbApp.getServerConfiguration().getSecurityManagerConfiguration().getPasswordPolicyConfiguration(); + } + + @Property + public boolean isForceHttps() { + return cbApp.getServerConfiguration().isForceHttps(); + } + + @NotNull + @Property + public List getSupportedHosts() { + return cbApp.getServerConfiguration().getSupportedHosts(); + } + + @NotNull + @Property + public String getBindSessionToIp() { + return cbApp.getServerConfiguration().getBindSessionToIp(); + } +} diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/WebDatasourceAccessCheckHandler.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/WebDatasourceAccessCheckHandler.java new file mode 100644 index 0000000000..a8400526e5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/WebDatasourceAccessCheckHandler.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model; + +import io.cloudbeaver.model.config.CBAppConfig; +import io.cloudbeaver.model.utils.ConfigurationUtils; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.utils.ServletAppUtils; +import org.jkiss.dbeaver.model.connection.DBPDriver; + +public class WebDatasourceAccessCheckHandler extends BaseDatasourceAccessCheckHandler { + @Override + protected boolean isDriverDisabled(DBPDriver driver) { + if (!ServletAppUtils.getServletApplication().isMultiuser()) { + return false; + } + CBAppConfig config = CBApplication.getInstance().getAppConfiguration(); + return !ConfigurationUtils.isDriverEnabled( + driver, + config.getEnabledDrivers(), + config.getDisabledDrivers()); + } +} diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/config/CBServerConfig.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/config/CBServerConfig.java new file mode 100644 index 0000000000..3f3f26e981 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/config/CBServerConfig.java @@ -0,0 +1,241 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model.config; + +import com.google.common.net.InetAddresses; +import com.google.gson.annotations.SerializedName; +import io.cloudbeaver.auth.CBAuthConstants; +import io.cloudbeaver.model.app.WebServerConfiguration; +import io.cloudbeaver.server.CBConstants; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; + +import java.net.URI; +import java.util.*; + +public class CBServerConfig implements WebServerConfiguration { + + private static final Log log = Log.getLog(CBServerConfig.class); + + protected String serverURL; + @NotNull + protected List supportedHosts = new ArrayList<>(); + protected boolean forceHttps; + protected int serverPort = CBConstants.DEFAULT_SERVER_PORT; + private String serverHost = null; + private String serverName = null; + private String sslConfigurationPath = null; + private String contentRoot = CBConstants.DEFAULT_CONTENT_ROOT; + private String rootURI = CBConstants.DEFAULT_ROOT_URI; + private String serviceURI = CBConstants.DEFAULT_SERVICES_URI; + + private String driversLocation = CBConstants.DEFAULT_DRIVERS_LOCATION; + @SerializedName("expireSessionAfterPeriod") + private long maxSessionIdleTime = CBAuthConstants.MAX_SESSION_IDLE_TIME; + private boolean develMode = false; + private boolean enableSecurityManager = false; + private final Map productSettings = new HashMap<>(); + + @SerializedName("sm") + protected SMControllerConfiguration securityManagerConfiguration; + @SerializedName("database") + private WebDatabaseConfig databaseConfiguration = new WebDatabaseConfig(); + private String staticContent = ""; + @NotNull + private String bindSessionToIp = CBConstants.BIND_SESSION_DISABLE; + + public CBServerConfig() { + this.securityManagerConfiguration = createSecurityManagerConfiguration(); + } + + public String getServerURL() { + return serverURL; + } + + public int getServerPort() { + return serverPort; + } + + public String getServerHost() { + return serverHost; + } + + public String getServerName() { + return serverName; + } + + public String getSslConfigurationPath() { + return sslConfigurationPath; + } + + public String getContentRoot() { + return contentRoot; + } + + public String getRootURI() { + return rootURI; + } + + public String getServicesURI() { + return serviceURI; + } + + public String getDriversLocation() { + return driversLocation; + } + + public WebDatabaseConfig getDatabaseConfiguration() { + return databaseConfiguration; + } + + public String getStaticContent() { + return staticContent; + } + + public void setServerURL(String serverURL) { + this.serverURL = serverURL; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + public void setServerHost(String serverHost) { + this.serverHost = serverHost; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public void setSslConfigurationPath(String sslConfigurationPath) { + this.sslConfigurationPath = sslConfigurationPath; + } + + public void setContentRoot(String contentRoot) { + this.contentRoot = contentRoot; + } + + public void setRootURI(String rootURI) { + this.rootURI = rootURI; + } + + public void setServicesURI(String servicesURI) { + this.serviceURI = servicesURI; + } + + public void setDriversLocation(String driversLocation) { + this.driversLocation = driversLocation; + } + + public void setMaxSessionIdleTime(long maxSessionIdleTime) { + this.maxSessionIdleTime = maxSessionIdleTime; + } + + public void setDevelMode(boolean develMode) { + this.develMode = develMode; + } + + public void setEnableSecurityManager(boolean enableSecurityManager) { + this.enableSecurityManager = enableSecurityManager; + } + + public void setDatabaseConfiguration(WebDatabaseConfig databaseConfiguration) { + this.databaseConfiguration = databaseConfiguration; + } + + public void setStaticContent(String staticContent) { + this.staticContent = staticContent; + } + + @Override + public boolean isDevelMode() { + return develMode; + } + + public long getMaxSessionIdleTime() { + return maxSessionIdleTime; + } + + public boolean isEnableSecurityManager() { + return enableSecurityManager; + } + + @NotNull + public Map getProductSettings() { + return productSettings; + } + + public T getSecurityManagerConfiguration() { + return (T) securityManagerConfiguration; + } + + protected SMControllerConfiguration createSecurityManagerConfiguration() { + return new SMControllerConfiguration(); + } + + public boolean isForceHttps() { + return forceHttps; + } + + public void setForceHttps(boolean forceHttps) { + this.forceHttps = forceHttps; + } + + @NotNull + public List getSupportedHosts() { + return new ArrayList<>(supportedHosts); + } + + public void setSupportedHosts(@NotNull Collection availableHosts) { + LinkedHashSet uniqueHosts = new LinkedHashSet<>(); + for (String host : availableHosts) { + try { + if (!host.startsWith("http://") && !host.startsWith("https://")) { + host = "http://" + host; // Default to HTTP if no scheme is provided to avoid uri parse exception + } + URI uri = URI.create(host); + String hostName = uri.getHost() != null ? uri.getHost() : host; + if (InetAddresses.isInetAddress(hostName)) { + log.warn("Host URI contains an IP address: " + hostName + ", skipped."); + continue; + } + StringBuilder hostNameBuilder = new StringBuilder(hostName); + + if (uri.getPort() > 0) { + hostNameBuilder.append(':') + .append(uri.getPort()); + } + + uniqueHosts.add(hostNameBuilder.toString()); + } catch (Exception e) { + log.error("Invalid host URI: " + host, e); + } + } + this.supportedHosts.clear(); + this.supportedHosts.addAll(uniqueHosts); + } + + @NotNull + public String getBindSessionToIp() { + return bindSessionToIp; + } + + public void setBindSessionToIp(@NotNull String bindSessionToIp) { + this.bindSessionToIp = bindSessionToIp; + } +} diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplication.java new file mode 100644 index 0000000000..62be954fbd --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplication.java @@ -0,0 +1,806 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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; + +import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.auth.NoAuthCredentialsProvider; +import io.cloudbeaver.model.CBWebServerConfig; +import io.cloudbeaver.model.WebServerConfig; +import io.cloudbeaver.model.app.BaseServletApplication; +import io.cloudbeaver.model.app.ServletAuthApplication; +import io.cloudbeaver.model.app.ServletAuthConfiguration; +import io.cloudbeaver.model.app.ServletSystemInformationCollector; +import io.cloudbeaver.model.config.CBAppConfig; +import io.cloudbeaver.model.config.CBServerConfig; +import io.cloudbeaver.registry.WebDriverRegistry; +import io.cloudbeaver.registry.WebFeatureRegistry; +import io.cloudbeaver.registry.WebServiceRegistry; +import io.cloudbeaver.server.jetty.CBJettyServer; +import io.cloudbeaver.service.ConnectionController; +import io.cloudbeaver.service.ConnectionControllerCE; +import io.cloudbeaver.service.DBWServiceInitializer; +import io.cloudbeaver.service.DBWServiceServerConfigurator; +import io.cloudbeaver.service.session.CBSessionManager; +import io.cloudbeaver.utils.WebDataSourceUtils; +import org.eclipse.core.runtime.Platform; +import org.eclipse.osgi.service.datalocation.Location; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBConstants; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.app.DBPPlatform; +import org.jkiss.dbeaver.model.auth.AuthInfo; +import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.impl.app.BaseApplicationImpl; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.security.SMAdminController; +import org.jkiss.dbeaver.model.security.SMConstants; +import org.jkiss.dbeaver.model.security.SMObjectType; +import org.jkiss.dbeaver.model.websocket.event.WSEventController; +import org.jkiss.dbeaver.model.websocket.event.WSServerConfigurationChangedEvent; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.dbeaver.runtime.ui.DBPPlatformUI; +import org.jkiss.utils.ArrayUtils; +import org.jkiss.utils.CommonUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This class controls all aspects of the application's execution + */ +public abstract class CBApplication + extends BaseServletApplication + implements ServletAuthApplication, WebApplication { + + private static final Log log = Log.getLog(CBApplication.class); + + private static final boolean RECONFIGURATION_ALLOWED = true; + /** + * In configuration mode sessions expire after a week + */ + private static final long CONFIGURATION_MODE_SESSION_IDLE_TIME = 60 * 60 * 1000 * 24 * 7; + + + static { + Log.setDefaultDebugStream(System.out); + } + + + public static CBApplication getInstance() { + return (CBApplication) BaseApplicationImpl.getInstance(); + } + + private final File homeDirectory; + + // Persistence + protected SMAdminController securityController; + private boolean configurationMode = false; + protected String containerId; + private final List localInetAddresses = new ArrayList<>(); + + protected final WSEventController eventController = new WSEventController(); + + private CBSessionManager sessionManager; + + private final Map initActions = new ConcurrentHashMap<>(); + private ServletSystemInformationCollector systemInformationCollector; + + private CBJettyServer jettyServer; + + private final Map applicationContext = new ConcurrentHashMap<>(); + + public CBApplication() { + this.homeDirectory = new File(initHomeFolder()); + } + + public String getServerURL() { + return getServerConfiguration().getServerURL(); + } + + // Port this server listens on. If set the 0 a random port is assigned which may be obtained with getLocalPort() + @Override + public int getServerPort() { + return getServerConfiguration().getServerPort(); + } + + // The network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. + public String getServerHost() { + return getServerConfiguration().getServerHost(); + } + + public String getServerName() { + return getServerConfiguration().getServerName(); + } + + public String getRootURI() { + return getServerConfiguration().getRootURI(); + } + + public String getServicesURI() { + return getServerConfiguration().getServicesURI(); + } + + + public Path getHomeDirectory() { + return homeDirectory.toPath(); + } + + @Override + public boolean isMultiNode() { + return false; + } + + /** + * @return actual max session idle time + */ + public long getMaxSessionIdleTime() { + if (isConfigurationMode()) { + return CONFIGURATION_MODE_SESSION_IDLE_TIME; + } + return getServerConfiguration().getMaxSessionIdleTime(); + } + + /** + * @return max session idle time from server configuration, may differ from {@link #getMaxSessionIdleTime()} + */ + + public CBAppConfig getAppConfiguration() { + return getServerConfigurationController().getAppConfiguration(); + } + + public T getServerConfiguration() { + return getServerConfigurationController().getServerConfiguration(); + } + + @Override + public ServletAuthConfiguration getAuthConfiguration() { + return getAppConfiguration(); + } + + @Override + public String getAuthServiceUriSegment() { + return getServerConfigurationController().getAuthServiceURL(); + } + + @NotNull + public Map getProductConfiguration() { + return getServerConfigurationController().getProductConfiguration(); + } + + public SMAdminController getSecurityController() { + return securityController; + } + + @Override + protected void startServer() { + try { + if (!loadServerConfiguration()) { + return; + } + + if (CommonUtils.isEmpty(this.getAppConfiguration().getDefaultUserTeam())) { + throw new DBException("Default user team must be specified"); + } + } catch (DBException e) { + log.error(e); + return; + } + + configurationMode = CommonUtils.isEmpty(getServerConfiguration().getServerName()); + + try { + refreshServerConfiguration(); + } catch (DBException e) { + log.error("Error refreshing server configuration", e); + return; + } + + eventController.setForceSkipEvents(isConfigurationMode()); // do not send events if configuration mode is on + + + Location instanceLoc = Platform.getInstanceLocation(); + try { + if (!instanceLoc.isSet()) { // always false? + URL wsLocationURL = new URL( + "file", //$NON-NLS-1$ + null, + getWorkspaceDirectory().toAbsolutePath().toString()); + instanceLoc.set(wsLocationURL, true); + } + } catch (Exception e) { + log.error("Error setting workspace location to " + getWorkspaceDirectory().toAbsolutePath(), e); + return; + } + this.systemInformationCollector = createSystemInformationCollector(); + this.systemInformationCollector.setWorkspacePath(instanceLoc.getURL().toString()); + + log.debug("%s %s is starting".formatted( + systemInformationCollector.getProductName(), + systemInformationCollector.getProductVersion()) + ); //$NON-NLS-1$ + log.debug("\tOS: " + systemInformationCollector.getOsInfo()); + log.debug("\tJava version: " + systemInformationCollector.getJavaVersion()); + log.debug("\tInstall path: '" + systemInformationCollector.getInstallPath() + "'"); //$NON-NLS-1$ //$NON-NLS-2$ + log.debug("\tGlobal workspace: '" + systemInformationCollector.getWorkspacePath() + "'"); //$NON-NLS-1$ //$NON-NLS-2$ + log.debug("\tMemory available " + systemInformationCollector.getMemoryAvailable()); + + DBWorkbench.getPlatform().getApplication(); + + log.debug("\tContent root: " + new File(getServerConfiguration().getContentRoot()).getAbsolutePath()); + log.debug("\tDrivers storage: " + new File(getServerConfiguration().getDriversLocation()).getAbsolutePath()); + //log.debug("\tDrivers root: " + driversLocation); + //log.debug("\tProduct details: " + application.getInfoDetails()); + log.debug("\tListen port: " + getServerPort() + (CommonUtils.isEmpty(getServerHost()) ? " on all interfaces" : " on " + getServerHost())); + log.debug("\tBase URI: " + getServicesURI()); + if (!isConfigurationMode()) { + log.debug("\tGlobal access server URL: " + getServerConfiguration().getServerURL()); + } + if (isDevelMode()) { + log.debug("\tDevelopment mode"); + } else { + log.debug("\tProduction mode"); + } + if (configurationMode) { + log.debug("\tServer is in configuration mode!"); + } + { + determineLocalAddresses(); + log.debug("\tLocal host addresses:"); + for (InetAddress ia : localInetAddresses) { + log.debug("\t\t" + ia.getHostAddress() + " (" + ia.getCanonicalHostName() + ")"); + } + } + { + // Perform services initialization + for (DBWServiceInitializer wsi : WebServiceRegistry.getInstance() + .getWebServices(DBWServiceInitializer.class)) { + try { + wsi.initializeService(this); + } catch (Exception e) { + log.warn("Error initializing web service " + wsi.getClass().getName(), e); + } + } + + } + + try { + initializeServer(); + } catch (DBException e) { + log.error("Error initializing server", e); + return; + } + + try { + initializeSecurityController(); + } catch (Exception e) { + log.error("Error initializing database", e); + return; + } + + + if (configurationMode) { + // Try to configure automatically + performAutoConfiguration(getMainConfigurationFilePath().getParent()); + } else if (!isMultiNode()) { + var appConfiguration = getServerConfigurationController().getAppConfiguration(); + if (appConfiguration.isGrantConnectionsAccessToAnonymousTeam()) { + grantAnonymousAccessToConnections(appConfiguration, CBConstants.ADMIN_AUTO_GRANT); + } + grantPermissionsToConnections(); + } + try { + this.systemInformationCollector.collectInternalDatabaseUseInformation(); + } catch (DBException e) { + log.error("Error collecting system information", e); + } + + eventController.scheduleCheckJob(); + + runWebServer(); + + log.debug("Shutdown"); + } + + private void refreshServerConfiguration() throws DBException { + refreshDisabledDriversConfig(); + refreshEnabledFeatures(); + if (!isConfigurationMode()) { + flushConfiguration(); + } + } + + private void refreshEnabledFeatures() { + Set enabledFeatures = new LinkedHashSet<>(Arrays.asList(getAppConfiguration().getEnabledFeatures())); + Set disabledFeatures = new LinkedHashSet<>(Arrays.asList(getAppConfiguration().getDisabledFeatures())); + + WebFeatureRegistry.getInstance().getWebFeatures().stream() + .filter(f -> f.isEnabledByDefault() && !disabledFeatures.contains(f.getId())) + .forEach(f -> enabledFeatures.add(f.getId())); + + getAppConfiguration().setEnabledFeatures(enabledFeatures.toArray(new String[0])); + } + + protected ServletSystemInformationCollector createSystemInformationCollector() { + return new ServletSystemInformationCollector<>(this); + } + + /** + * Configures server automatically. + * Called on startup + */ + protected void performAutoConfiguration(Path configPath) { + String autoServerName = System.getenv(CBConstants.VAR_AUTO_CB_SERVER_NAME); + String autoServerURL = System.getenv(CBConstants.VAR_AUTO_CB_SERVER_URL); + String autoAdminName = System.getenv(CBConstants.VAR_AUTO_CB_ADMIN_NAME); + String autoAdminPassword = System.getenv(CBConstants.VAR_AUTO_CB_ADMIN_PASSWORD); + + if (CommonUtils.isEmpty(autoServerName) || CommonUtils.isEmpty(autoAdminName) || CommonUtils.isEmpty( + autoAdminPassword)) { + // Try to load from auto config file + if (Files.exists(configPath)) { + Path autoConfigFile = configPath.resolve(CBConstants.AUTO_CONFIG_FILE_NAME); + if (Files.exists(autoConfigFile)) { + Properties autoProps = new Properties(); + try (InputStream is = Files.newInputStream(autoConfigFile)) { + autoProps.load(is); + + autoServerName = autoProps.getProperty(CBConstants.VAR_AUTO_CB_SERVER_NAME); + autoServerURL = autoProps.getProperty(CBConstants.VAR_AUTO_CB_SERVER_URL); + autoAdminName = autoProps.getProperty(CBConstants.VAR_AUTO_CB_ADMIN_NAME); + autoAdminPassword = autoProps.getProperty(CBConstants.VAR_AUTO_CB_ADMIN_PASSWORD); + } catch (IOException e) { + log.error("Error loading auto configuration file '" + autoConfigFile + "'", + e); + } + } + } + } + + if (CommonUtils.isEmpty(autoServerName) || CommonUtils.isEmpty(autoAdminName) || CommonUtils.isEmpty( + autoAdminPassword)) { + log.info("No auto configuration was found. Server must be configured manually"); + return; + } + CBServerConfig serverConfig = new CBServerConfig(); + serverConfig.setServerName(autoServerName); + serverConfig.setServerURL(autoServerURL); + serverConfig.setMaxSessionIdleTime(getMaxSessionIdleTime()); + try { + finishConfiguration( + autoAdminName, + autoAdminPassword, + Collections.emptyList(), + serverConfig, + getAppConfiguration(), + null + ); + } catch (Exception e) { + log.error("Error loading server auto configuration", e); + } + } + + protected void initializeServer() throws DBException { + for (DBWServiceServerConfigurator wsc : WebServiceRegistry.getInstance() + .getWebServices(DBWServiceServerConfigurator.class)) { + try { + wsc.migrateConfigurationIfNeeded(this); + } catch (Exception e) { + log.warn("Error migration configuration " + wsc.getClass().getName(), e); + } + } + } + + private void determineLocalAddresses() { + try { + try { + InetAddress dockerAddress = InetAddress.getByName(CBConstants.VAR_HOST_DOCKER_INTERNAL); + localInetAddresses.add(dockerAddress); + log.debug("\tRun in Docker container (" + dockerAddress + ")?"); + } catch (UnknownHostException e) { + // Ignore - not a docker env + } + + boolean hasLoopbackAddress = false; + for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { + NetworkInterface intf = en.nextElement(); + for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { + InetAddress localInetAddress = enumIpAddr.nextElement(); + boolean loopbackAddress = localInetAddress.isLoopbackAddress(); + if (loopbackAddress ? !hasLoopbackAddress : !localInetAddress.isLinkLocalAddress()) { + if (loopbackAddress) { + hasLoopbackAddress = true; + } + localInetAddresses.add(localInetAddress); + } + } + } + } catch (Exception e) { + log.error(e); + } + + } + + @NotNull + public Path getDataDirectory(boolean create) { + Path dataDir = getWorkspaceDirectory().resolve(CBConstants.RUNTIME_DATA_DIR_NAME); + if (create && !Files.exists(dataDir)) { + try { + Files.createDirectories(dataDir); + } catch (IOException e) { + log.error("Can't create data directory '" + dataDir.toAbsolutePath() + "'"); + + } + } + return dataDir; + } + + private void initializeSecurityController() throws DBException { + securityController = createGlobalSecurityController(); + } + + protected abstract SMAdminController createGlobalSecurityController() throws DBException; + + @NotNull + protected String initHomeFolder() { + String homeFolder = System.getenv(CBConstants.ENV_CB_HOME); + if (CommonUtils.isEmpty(homeFolder)) { + homeFolder = System.getProperty("user.dir"); + } + if (CommonUtils.isEmpty(homeFolder)) { + homeFolder = "."; + } + return homeFolder; + } + + private void runWebServer() { + log.debug( + String.format("Starting Jetty server (%d on %s) ", + getServerPort(), + CommonUtils.isEmpty(getServerHost()) ? "all interfaces" : getServerHost()) + ); + this.jettyServer = new CBJettyServer(this); + this.jettyServer.runServer(); + } + + + @Override + public void stop() { + shutdown(); + } + + protected void shutdown() { + log.debug("Cloudbeaver Server is stopping"); //$NON-NLS-1$ + } + + @Override + public String getInfoDetails(DBRProgressMonitor monitor) { + return ""; + } + + @Override + public String getDefaultProjectName() { + return "GlobalConfiguration"; + } + + public boolean isDevelMode() { + return getServerConfiguration().isDevelMode(); + } + + public boolean isConfigurationMode() { + return configurationMode; + } + + public String getLocalHostAddress() { + return getServerConfigurationController().getLocalHostAddress(); + } + + public List getLocalInetAddresses() { + return localInetAddresses; + } + + public synchronized void finishConfiguration( + @NotNull String adminName, + @Nullable String adminPassword, + @NotNull List authInfoList, + @NotNull CBServerConfig serverConfig, + @NotNull CBAppConfig appConfig, + @Nullable SMCredentialsProvider credentialsProvider + ) throws DBException { + if (!RECONFIGURATION_ALLOWED && !isConfigurationMode()) { + throw new DBException("Application must be in configuration mode"); + } + + if (isConfigurationMode()) { + finishSecurityServiceConfiguration(adminName.toLowerCase(), adminPassword, authInfoList); + } + + // Save runtime configuration + log.debug("Saving runtime configuration"); + getServerConfigurationController().saveRuntimeConfig(serverConfig, appConfig, credentialsProvider); + + // Grant permissions to predefined connections + if (appConfig.isGrantConnectionsAccessToAnonymousTeam()) { + grantAnonymousAccessToConnections(appConfig, adminName); + } + reloadConfiguration(credentialsProvider); + } + + public synchronized void reloadConfiguration(@Nullable SMCredentialsProvider credentialsProvider) + throws DBException { + // Re-load runtime configuration + try { + Path runtimeAppConfigPath = getServerConfigurationController().getRuntimeAppConfigPath(); + log.debug("Reloading application configuration"); + getServerConfigurationController().loadConfiguration(runtimeAppConfigPath); + } catch (Exception e) { + throw new DBException("Error parsing configuration", e); + } + + configurationMode = CommonUtils.isEmpty(getServerName()); + + // Reloading configuration by services + for (DBWServiceServerConfigurator wsc : WebServiceRegistry.getInstance() + .getWebServices(DBWServiceServerConfigurator.class)) { + try { + wsc.reloadConfiguration(getAppConfiguration()); + } catch (Exception e) { + log.warn("Error reloading configuration by web service " + wsc.getClass().getName(), e); + } + } + + sendConfigChangedEvent(credentialsProvider); + eventController.setForceSkipEvents(isConfigurationMode()); + if (this.jettyServer != null) { + this.jettyServer.refreshJettyConfig(); + } + } + + protected abstract void finishSecurityServiceConfiguration( + @NotNull String adminName, + @Nullable String adminPassword, + @NotNull List authInfoList + ) throws DBException; + + public synchronized void flushConfiguration(SMCredentialsProvider webSession) throws DBException { + getServerConfigurationController().saveRuntimeConfig(webSession); + } + + public synchronized void flushConfiguration() throws DBException { + getServerConfigurationController().saveRuntimeConfig(new NoAuthCredentialsProvider()); + } + + + private void grantAnonymousAccessToConnections(CBAppConfig appConfig, String adminName) { + try { + String anonymousTeamId = appConfig.getAnonymousUserTeam(); + var securityController = getSecurityController(); + for (DBPDataSourceContainer ds : WebServiceUtils.getGlobalDataSourceRegistry().getDataSources()) { + var datasourcePermissions = securityController.getObjectPermissions(anonymousTeamId, + ds.getId(), + SMObjectType.datasource); + if (ArrayUtils.isEmpty(datasourcePermissions.getPermissions())) { + securityController.setObjectPermissions( + Set.of(ds.getId()), + SMObjectType.datasource, + Set.of(anonymousTeamId), + Set.of(SMConstants.DATA_SOURCE_ACCESS_PERMISSION), + adminName + ); + } + } + } catch (Exception e) { + log.error("Error granting anonymous access to connections", e); + } + } + + private void grantPermissionsToConnections() { + try { + var globalRegistry = WebDataSourceUtils.getGlobalDataSourceRegistry(); + var permissionsConfiguration = getServerConfigurationController().readConnectionsPermissionsConfiguration( + globalRegistry.getProject().getMetadataFolder(false)); + + if (permissionsConfiguration == null) { + return; + } + for (var entry : permissionsConfiguration.entrySet()) { + var dataSourceId = entry.getKey(); + var ds = globalRegistry.getDataSource(dataSourceId); + if (ds == null) { + log.error("Connection " + dataSourceId + " is not found in project " + globalRegistry.getProject() + .getName()); + } + List permissions = JSONUtils.getStringList(permissionsConfiguration, dataSourceId); + var securityController = getSecurityController(); + securityController.deleteAllObjectPermissions(dataSourceId, SMObjectType.datasource); + securityController.setObjectPermissions( + Set.of(dataSourceId), + SMObjectType.datasource, + new HashSet<>(permissions), + Set.of(SMConstants.DATA_SOURCE_ACCESS_PERMISSION), + CBConstants.ADMIN_AUTO_GRANT + ); + } + } catch (DBException e) { + log.error("Error granting permissions to connections", e); + } + } + + //////////////////////////////////////////////////////////////////////// + // License management + + @Override + public boolean isLicenseRequired() { + return false; + } + + public boolean isLicenseValid() { + return false; + } + + @Nullable + public String getLicenseStatus() { + return null; + } + + public CBSessionManager getSessionManager() { + if (sessionManager == null) { + sessionManager = createSessionManager(); + } + return sessionManager; + } + + protected CBSessionManager createSessionManager() { + return new CBSessionManager(this); + } + + @NotNull + public WebDriverRegistry getDriverRegistry() { + return WebDriverRegistry.getInstance(); + } + + public List getAvailableAuthRoles() { + return List.of(); + } + + public List getAvailableTeamRoles() { + return List.of(); + } + + @Override + public WSEventController getEventController() { + return eventController; + } + + @Nullable + public String getDefaultAuthRole() { + return null; + } + + public String getContainerId() { + if (containerId == null) { + containerId = System.getenv("HOSTNAME"); + } + return containerId; + } + + @NotNull + @Override + public Class getPlatformClass() { + return CBPlatform.class; + } + + @Override + public Class getPlatformUIClass() { + return ServletPlatformUI.class; + } + + public void saveProductConfiguration( + SMCredentialsProvider credentialsProvider, + Map productConfiguration + ) throws DBException { + getServerConfigurationController().saveProductConfiguration(productConfiguration); + flushConfiguration(credentialsProvider); + sendConfigChangedEvent(credentialsProvider); + } + + protected void sendConfigChangedEvent(SMCredentialsProvider credentialsProvider) { + String sessionId = null; + if (credentialsProvider != null && credentialsProvider.getActiveUserCredentials() != null) { + sessionId = credentialsProvider.getActiveUserCredentials().getSmSessionId(); + } + eventController.addEvent(new WSServerConfigurationChangedEvent(sessionId, null)); + } + + @Override + public abstract CBServerConfigurationController getServerConfigurationController(); + + private void refreshDisabledDriversConfig() { + getDriverRegistry().refreshApplicableDrivers(); + CBAppConfig config = getAppConfiguration(); + Set disabledDrivers = new LinkedHashSet<>(Arrays.asList(config.getDisabledDrivers())); + for (DBPDriver driver : getDriverRegistry().getApplicableDrivers()) { + boolean isSafeEmbedded = CommonUtils.toBoolean(driver.getDriverParameter(DBConstants.PARAM_SAFE_EMBEDDED_DRIVER), false); + boolean isNotEmbeddedOrForced = !driver.isEmbedded() || config.isDriverForceEnabled(driver.getFullId()); + if (isSafeEmbedded || isNotEmbeddedOrForced) { + continue; + } + disabledDrivers.add(driver.getFullId()); + } + config.setDisabledDrivers(disabledDrivers.toArray(new String[0])); + } + + @Override + public boolean isEnvironmentVariablesAccessible() { + return getAppConfiguration().isSystemVariablesResolvingEnabled(); + } + + @Override + public boolean isInitializationMode() { + return !initActions.isEmpty(); + } + + public void addInitAction(@NotNull String actionId, @NotNull String description) { + initActions.put(actionId, description); + } + + public void removeInitAction(@NotNull String actionId) { + initActions.remove(actionId); + } + + public Map getInitActions() { + return Map.copyOf(initActions); + } + + @Override + public WebServerConfig getWebServerConfig() { + return new CBWebServerConfig(this); + } + + @Override + public ConnectionController getConnectionController() { + return new ConnectionControllerCE(); + } + + @NotNull + @Override + public ServletSystemInformationCollector getSystemInformationCollector() { + return systemInformationCollector; + } + + public void addApplicationContextValue(@NotNull String key, @NotNull Object value) { + applicationContext.put(key, value); + } + + @Nullable + public T getApplicationContextValue(@NotNull String key) { + return (T) applicationContext.get(key); + } +} diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplicationCE.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplicationCE.java new file mode 100644 index 0000000000..f69512b7c5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplicationCE.java @@ -0,0 +1,119 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import io.cloudbeaver.auth.NoAuthCredentialsProvider; +import io.cloudbeaver.model.CBWebServerConfig; +import io.cloudbeaver.model.WebServerConfig; +import io.cloudbeaver.model.config.CBServerConfig; +import io.cloudbeaver.model.rm.local.LocalResourceController; +import io.cloudbeaver.service.security.CBEmbeddedSecurityController; +import io.cloudbeaver.service.security.EmbeddedSecurityControllerFactory; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBFileController; +import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.dbeaver.model.auth.AuthInfo; +import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +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.LocalFileController; +import org.jkiss.dbeaver.runtime.DBWorkbench; + +import java.util.List; + +public class CBApplicationCE extends CBApplication { + private static final Log log = Log.getLog(CBApplicationCE.class); + + private final CBServerConfigurationControllerEmbedded serverConfigController; + + public CBApplicationCE() { + super(); + this.serverConfigController = new CBServerConfigurationControllerEmbedded<>(new CBServerConfig(), getHomeDirectory()); + } + + @Override + public SMController createSecurityController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException { + return new EmbeddedSecurityControllerFactory<>().createSecurityService( + this, + getServerConfiguration().getDatabaseConfiguration(), + credentialsProvider, + getServerConfiguration().getSecurityManagerConfiguration() + ); + } + @Override + public SMAdminController getAdminSecurityController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException { + return new EmbeddedSecurityControllerFactory<>().createSecurityService( + this, + getServerConfiguration().getDatabaseConfiguration(), + credentialsProvider, + getServerConfiguration().getSecurityManagerConfiguration() + ); + } + + protected SMAdminController createGlobalSecurityController() throws DBException { + return new EmbeddedSecurityControllerFactory<>().createSecurityService( + this, + getServerConfiguration().getDatabaseConfiguration(), + new NoAuthCredentialsProvider(), + getServerConfiguration().getSecurityManagerConfiguration() + ); + } + + + + @Override + public RMController createResourceController(@NotNull SMCredentialsProvider credentialsProvider, + @NotNull DBPWorkspace workspace) throws DBException { + return LocalResourceController.builder(credentialsProvider, workspace, this::getSecurityController).build(); + } + + @NotNull + @Override + public DBFileController createFileController(@NotNull SMCredentialsProvider credentialsProvider) { + return new LocalFileController(DBWorkbench.getPlatform().getWorkspace().getAbsolutePath().resolve(DBFileController.DATA_FOLDER)); + } + + @Override + public CBServerConfigurationControllerEmbedded getServerConfigurationController() { + return serverConfigController; + } + + protected void shutdown() { + try { + if (securityController instanceof CBEmbeddedSecurityController embeddedSecurityController) { + embeddedSecurityController.shutdown(); + } + } catch (Exception e) { + log.error(e); + } + super.shutdown(); + } + + protected void finishSecurityServiceConfiguration( + @NotNull String adminName, + @Nullable String adminPassword, + @NotNull List authInfoList + ) throws DBException { + if (securityController instanceof CBEmbeddedSecurityController embeddedSecurityController) { + embeddedSecurityController.finishConfiguration(adminName, adminPassword, authInfoList); + } + } +} diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatform.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatform.java new file mode 100644 index 0000000000..e2814f8fcf --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatform.java @@ -0,0 +1,118 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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; + +import io.cloudbeaver.server.jobs.SessionStateJob; +import io.cloudbeaver.server.jobs.WebDataSourceMonitorJob; +import io.cloudbeaver.server.jobs.WebSessionMonitorJob; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.impl.app.BaseApplicationImpl; +import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; +import org.jkiss.dbeaver.model.runtime.AbstractJob; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.utils.IOUtils; + +import java.io.IOException; + +/** + * CBPlatform + */ +public class CBPlatform extends BaseWebPlatform { + + // The plug-in ID + public static final String PLUGIN_ID = "io.cloudbeaver.server"; //$NON-NLS-1$ + + private static final Log log = Log.getLog(CBPlatform.class); + + private WebServerPreferenceStore preferenceStore; + + public static CBPlatform getInstance() { + return (CBPlatform) DBWorkbench.getPlatform(); + } + + protected CBPlatform() { + } + + @Override + protected synchronized void initialize() { + long startTime = System.currentTimeMillis(); + log.info("Initialize web platform...: "); + this.preferenceStore = new WebServerPreferenceStore(WebPlatformActivator.getInstance().getPreferences()); + super.initialize(); + scheduleServerJobs(); + log.info("Web platform initialized (" + (System.currentTimeMillis() - startTime) + "ms)"); + } + + protected void scheduleServerJobs() { + super.scheduleServerJobs(); + new WebSessionMonitorJob(this, getApplication().getSessionManager()) + .scheduleMonitor(); + + new SessionStateJob(this, getApplication().getSessionManager()) + .scheduleMonitor(); + + new WebDataSourceMonitorJob(this, getApplication().getSessionManager()) + .scheduleMonitor(); + + new AbstractJob("Delete temp folder") { + @Override + protected IStatus run(DBRProgressMonitor monitor) { + try { + IOUtils.deleteDirectory(getTempFolder(monitor, TEMP_FILE_FOLDER)); + IOUtils.deleteDirectory(getTempFolder(monitor, TEMP_FILE_IMPORT_FOLDER)); + } catch (IOException e) { + throw new RuntimeException(e); + } + return Status.OK_STATUS; + } + }.schedule(); + } + + public synchronized void dispose() { + long startTime = System.currentTimeMillis(); + log.debug("Shutdown Core..."); + + super.dispose(); + + System.gc(); + log.debug("Shutdown completed in " + (System.currentTimeMillis() - startTime) + "ms"); + } + + @NotNull + @Override + public CBApplication getApplication() { + return (CBApplication) BaseApplicationImpl.getInstance(); + } + + + @NotNull + @Override + public DBPPreferenceStore getPreferenceStore() { + return preferenceStore; + } + + @Override + public boolean isShuttingDown() { + return false; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatformActivator.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatformActivator.java similarity index 81% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatformActivator.java rename to server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatformActivator.java index 4f86d3bda0..cee656629f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatformActivator.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatformActivator.java @@ -1,8 +1,6 @@ -package io.cloudbeaver.server; - /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,6 +15,10 @@ * limitations under the License. */ +package io.cloudbeaver.server; + +import org.jkiss.dbeaver.runtime.DBWorkbench; + /** * The activator class controls the plug-in life cycle */ @@ -26,8 +28,8 @@ public class CBPlatformActivator extends WebPlatformActivator { protected void shutdownPlatform() { try { // Dispose core - if (CBPlatform.instance != null) { - CBPlatform.instance.dispose(); + if (DBWorkbench.isPlatformStarted() && DBWorkbench.getPlatform() instanceof CBPlatform cbPlatform) { + cbPlatform.dispose(); } } catch (Throwable e) { e.printStackTrace(); diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBServerConfigurationController.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBServerConfigurationController.java new file mode 100644 index 0000000000..338680eff3 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBServerConfigurationController.java @@ -0,0 +1,679 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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; + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import io.cloudbeaver.model.app.BaseServerConfigurationController; +import io.cloudbeaver.model.app.BaseServletApplication; +import io.cloudbeaver.model.config.CBAppConfig; +import io.cloudbeaver.model.config.CBServerConfig; +import io.cloudbeaver.model.config.PasswordPolicyConfiguration; +import io.cloudbeaver.model.config.SMControllerConfiguration; +import io.cloudbeaver.utils.ServletAppUtils; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.ModelPreferences; +import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; +import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.dbeaver.runtime.IVariableResolver; +import org.jkiss.dbeaver.utils.ContentUtils; +import org.jkiss.dbeaver.utils.PrefUtils; +import org.jkiss.dbeaver.utils.SystemVariablesResolver; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public abstract class CBServerConfigurationController + extends BaseServerConfigurationController { + + private static final Log log = Log.getLog(CBServerConfigurationController.class); + + // Configurations + @NotNull + private final T serverConfiguration; + private final CBAppConfig appConfiguration = new CBAppConfig(); + @NotNull + protected final Path homeDirectory; + private final Map externalProperties = new LinkedHashMap<>(); + private final Map originalConfigurationProperties = new LinkedHashMap<>(); + private String localHostAddress; + + protected CBServerConfigurationController(@NotNull T serverConfiguration, @NotNull Path homeDirectory) { + super(homeDirectory); + this.serverConfiguration = serverConfiguration; + this.homeDirectory = homeDirectory; + } + + public String getAuthServiceURL() { + return serverConfiguration.getServicesURI(); + } + + @Override + public void loadServerConfiguration(Path configPath) throws DBException { + log.debug("Using configuration [" + configPath + "]"); + // Determine address for local host + localHostAddress = System.getenv(CBConstants.VAR_CB_LOCAL_HOST_ADDR); + if (CommonUtils.isEmpty(localHostAddress)) { + localHostAddress = System.getProperty(CBConstants.VAR_CB_LOCAL_HOST_ADDR); + } + if (CommonUtils.isEmpty(localHostAddress) || CBConstants.HOST_127_0_0_1.equals(localHostAddress) || "::0".equals( + localHostAddress)) { + localHostAddress = CBConstants.HOST_LOCALHOST; + } + + if (!Files.exists(configPath)) { + log.error("Configuration file " + configPath + " doesn't exist. Use defaults."); + } else { + loadConfiguration(configPath); + } + + // Try to load configuration from runtime app config file + Path runtimeConfigPath = getRuntimeAppConfigPath(); + if (Files.exists(runtimeConfigPath)) { + log.debug("Runtime configuration [" + runtimeConfigPath.toAbsolutePath() + "]"); + loadConfiguration(runtimeConfigPath); + } + + // Set default preferences + PrefUtils.setDefaultPreferenceValue(DBWorkbench.getPlatform().getPreferenceStore(), + ModelPreferences.UI_DRIVERS_HOME, + getServerConfiguration().getDriversLocation()); + validateFinalServerConfiguration(); + } + + @Nullable + @Override + public String getWorkspaceLocationFromEnv() { + String envValue = System.getenv("CLOUDBEAVER_WORKSPACE_LOCATION"); + return CommonUtils.nullIfEmpty(envValue); + } + + @NotNull + public Map loadConfiguration(Path configPath) throws DBException { + CBAppConfig prevConfig = new CBAppConfig(appConfiguration); + Map configProps = readConfiguration(configPath); + try { + parseConfiguration(configProps); + } catch (Exception e) { + throw new DBException("Error parsing server configuration", e); + } + + // Backward compatibility: load configs map + appConfiguration.loadLegacyCustomConfigs(); + + // Merge new config with old one + mergeOldConfiguration(prevConfig); + + patchConfigurationWithProperties(getServerConfiguration().getProductSettings()); + return configProps; + } + + protected void parseConfiguration(Map configProps) throws DBException { + Map serverConfig = JSONUtils.getObject(configProps, "server"); + + readExternalProperties(serverConfig); + patchConfigurationWithProperties(configProps); // patch again because properties can be changed + + Gson gson = getGson(); + Map currentConfigurationAsMap = gson.fromJson(gson.toJson(getServerConfiguration()), + JSONUtils.MAP_TYPE_TOKEN); + serverConfig = ServletAppUtils.mergeConfigurations(currentConfigurationAsMap, serverConfig); + gson.fromJson( + gson.toJson(serverConfig), + TypeToken.get(getServerConfiguration().getClass()).getType() + ); + + parseServerConfiguration(); + + //SM config + gson.fromJson( + gson.toJson(JSONUtils.getObject(serverConfig, CBConstants.PARAM_SM_CONFIGURATION)), + getServerConfiguration().getSecurityManagerConfiguration().getClass() + ); + // App config + Map appConfig = JSONUtils.getObject(configProps, "app"); + preValidateAppConfiguration(appConfig); + gson.fromJson(gson.toJson(appConfig), CBAppConfig.class); + readProductConfiguration(serverConfig, gson); + } + + public T parseServerConfiguration() { + var config = getServerConfiguration(); + if (config.getServerURL() == null) { + String hostName = config.getServerHost(); + if (CommonUtils.isEmpty(hostName)) { + hostName = getLocalHostAddress(); + } + config.setServerURL("http://" + hostName + ":" + config.getServerPort()); + } + + config.setContentRoot(ServletAppUtils.getRelativePath(config.getContentRoot(), homeDirectory)); + config.setRootURI(readRootUri(config.getRootURI())); + config.setDriversLocation(ServletAppUtils.getRelativePath(config.getDriversLocation(), homeDirectory)); + + String staticContentsFile = config.getStaticContent(); + if (!CommonUtils.isEmpty(staticContentsFile)) { + try { + config.setStaticContent(Files.readString(Path.of(staticContentsFile))); + } catch (IOException e) { + log.error("Error reading static contents from " + staticContentsFile, e); + } + } + return config; + } + + protected void preValidateAppConfiguration(Map appConfig) throws DBException { + + } + + + private void readExternalProperties(Map serverConfig) { + String externalPropertiesFile = JSONUtils.getString(serverConfig, CBConstants.PARAM_EXTERNAL_PROPERTIES); + if (!CommonUtils.isEmpty(externalPropertiesFile)) { + Properties props = new Properties(); + try (InputStream is = Files.newInputStream(Path.of(externalPropertiesFile))) { + props.load(is); + } catch (IOException e) { + log.error("Error loading external properties from " + externalPropertiesFile, e); + } + for (String propName : props.stringPropertyNames()) { + this.externalProperties.put(propName, props.getProperty(propName)); + } + } + } + + protected void mergeOldConfiguration(CBAppConfig prevConfig) { + Map mergedPlugins = Stream.concat( + prevConfig.getPlugins().entrySet().stream(), + appConfiguration.getPlugins().entrySet().stream() + ) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (o, o2) -> o2)); + appConfiguration.setPlugins(mergedPlugins); + + Set mergedAuthProviders = Stream.concat( + prevConfig.getAuthCustomConfigurations().stream(), + appConfiguration.getAuthCustomConfigurations().stream() + ) + .collect(Collectors.toCollection(LinkedHashSet::new)); + appConfiguration.setAuthProvidersConfigurations(mergedAuthProviders); + } + + protected void readProductConfiguration(Map serverConfig, Gson gson) + throws DBException { + // legacy configuration with path to product.conf file + if (!serverConfig.containsKey(CBConstants.PARAM_PRODUCT_SETTINGS) + && serverConfig.get(CBConstants.PARAM_PRODUCT_CONFIGURATION) instanceof String + ) { + String productConfigPath = ServletAppUtils.getRelativePath( + JSONUtils.getString( + serverConfig, + CBConstants.PARAM_PRODUCT_CONFIGURATION, + CBConstants.DEFAULT_PRODUCT_CONFIGURATION + ), + homeDirectory + ); + if (!CommonUtils.isEmpty(productConfigPath)) { + File productConfigFile = new File(productConfigPath); + if (!productConfigFile.exists()) { + log.error("Product configuration file not found (" + productConfigFile.getAbsolutePath() + "'"); + } else { + log.debug("Load product configuration from '" + productConfigFile.getAbsolutePath() + "'"); + try (Reader reader = new InputStreamReader(new FileInputStream(productConfigFile), + StandardCharsets.UTF_8)) { + serverConfiguration.getProductSettings() + .putAll(ServletAppUtils.flattenMap(JSONUtils.parseMap(gson, reader))); + } catch (Exception e) { + throw new DBException("Error reading product configuration", e); + } + } + } + } + + if (workspacePath != null && IOUtils.isFileFromDefaultFS(getWorkspacePath())) { + // Add product config from runtime + Path rtConfig = getRuntimeProductConfigFilePath(); + if (Files.exists(rtConfig)) { + log.debug("Load product runtime configuration from '" + rtConfig + "'"); + try (Reader reader = new InputStreamReader(Files.newInputStream(rtConfig), StandardCharsets.UTF_8)) { + var runtimeProductSettings = JSONUtils.parseMap(gson, reader); + var productSettings = serverConfiguration.getProductSettings(); + runtimeProductSettings.putAll(productSettings); + Map flattenConfig = ServletAppUtils.flattenMap(runtimeProductSettings); + productSettings.clear(); + productSettings.putAll(flattenConfig); + } catch (Exception e) { + throw new DBException("Error reading product runtime configuration", e); + } + } + } + } + + protected Map readConnectionsPermissionsConfiguration(Path parentPath) { + String permissionsConfigPath = ServletAppUtils.getRelativePath(CBConstants.DEFAULT_DATASOURCE_PERMISSIONS_CONFIGURATION, + parentPath); + File permissionsConfigFile = new File(permissionsConfigPath); + if (permissionsConfigFile.exists()) { + log.debug("Load permissions configuration from '" + permissionsConfigFile.getAbsolutePath() + "'"); + try (Reader reader = new InputStreamReader(new FileInputStream(permissionsConfigFile), + StandardCharsets.UTF_8)) { + return JSONUtils.parseMap(getGson(), reader); + } catch (Exception e) { + log.error("Error reading permissions configuration", e); + } + } + return null; + } + + protected Map readConfiguration(Path configPath) throws DBException { + Map configProps = new LinkedHashMap<>(); + if (Files.exists(configPath)) { + log.debug("Read configuration [" + configPath.toAbsolutePath() + "]"); + + configProps.putAll(readConfigurationFile(configPath)); + + if (originalConfigurationProperties.isEmpty()) { + originalConfigurationProperties.putAll(configProps); + } else { + var mergedOriginalConfigs = ServletAppUtils.mergeConfigurations( + originalConfigurationProperties, + configProps + ); + this.originalConfigurationProperties.clear(); + // saves original configuration file + this.originalConfigurationProperties.putAll(mergedOriginalConfigs); + } + + configProps.putAll(readConfigurationFile(configPath)); + patchConfigurationWithProperties(configProps); // patch original properties + } + return configProps; + } + + public Map readConfigurationFile(Path path) throws DBException { + try (Reader reader = new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8)) { + return JSONUtils.parseMap(getGson(), reader); + } catch (Exception e) { + throw new DBException("Error parsing server configuration", e); + } + } + + @NotNull + protected GsonBuilder getGsonBuilder() { + // Stupid way to populate existing objects but ok google (https://github.com/google/gson/issues/431) + InstanceCreator appConfigCreator = type -> appConfiguration; + InstanceCreator navSettingsCreator = type -> (DataSourceNavigatorSettings) appConfiguration.getDefaultNavigatorSettings(); + var securityManagerConfiguration = getServerConfiguration().getSecurityManagerConfiguration(); + InstanceCreator smConfigCreator = type -> securityManagerConfiguration; + InstanceCreator serverConfigCreator = type -> serverConfiguration; + InstanceCreator smPasswordPoliceConfigCreator = + type -> securityManagerConfiguration.getPasswordPolicyConfiguration(); + return new GsonBuilder() + .setStrictness(Strictness.LENIENT) + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .registerTypeAdapter(getServerConfiguration().getClass(), serverConfigCreator) + .registerTypeAdapter(CBAppConfig.class, appConfigCreator) + .registerTypeAdapter(DataSourceNavigatorSettings.class, navSettingsCreator) + .registerTypeAdapter(SMControllerConfiguration.class, smConfigCreator) + .registerTypeAdapter(PasswordPolicyConfiguration.class, smPasswordPoliceConfigCreator); + } + + public synchronized void saveRuntimeConfig(SMCredentialsProvider credentialsProvider) throws DBException { + saveRuntimeConfig( + serverConfiguration, + appConfiguration, + credentialsProvider + ); + } + + protected synchronized void saveRuntimeConfig( + @NotNull CBServerConfig serverConfig, + @NotNull CBAppConfig appConfig, + @Nullable SMCredentialsProvider credentialsProvider + ) throws DBException { + if (serverConfig.getServerName() == null) { + throw new DBException("Invalid server configuration, server name cannot be empty"); + } + Map configurationProperties = collectConfigurationProperties(serverConfig, appConfig); + writeRuntimeConfig(getRuntimeAppConfigPath(), configurationProperties); + } + + private synchronized void writeRuntimeConfig(Path runtimeConfigPath, Map configurationProperties) + throws DBException { + if (Files.exists(runtimeConfigPath)) { + ContentUtils.makeFileBackup(runtimeConfigPath); + } + + try (Writer out = new OutputStreamWriter(Files.newOutputStream(runtimeConfigPath), StandardCharsets.UTF_8)) { + Gson gson = new GsonBuilder() + .setStrictness(Strictness.LENIENT) + .setPrettyPrinting() + .create(); + gson.toJson(configurationProperties, out); + + } catch (IOException e) { + throw new DBException("Error writing runtime configuration", e); + } + } + + + public synchronized void updateServerUrl(@NotNull SMCredentialsProvider credentialsProvider, + @Nullable String newPublicUrl) throws DBException { + getServerConfiguration().setServerURL(newPublicUrl); + } + + protected Map collectConfigurationProperties( + @NotNull CBServerConfig serverConfig, + @NotNull CBAppConfig appConfig + ) { + Map rootConfig = new LinkedHashMap<>(); + { + var originServerConfig = BaseServletApplication.getServerConfigProps(this.originalConfigurationProperties); // get server properties from original configuration file + var serverConfigProperties = collectServerConfigProperties(serverConfig, originServerConfig); + rootConfig.put("server", serverConfigProperties); + } + { + var appConfigProperties = new LinkedHashMap(); + Map oldAppConfig = JSONUtils.getObject(this.originalConfigurationProperties, "app"); + rootConfig.put("app", appConfigProperties); + + copyConfigValue( + oldAppConfig, appConfigProperties, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled()); + copyConfigValue( + oldAppConfig, + appConfigProperties, + "supportsCustomConnections", + appConfig.isSupportsCustomConnections()); + copyConfigValue( + oldAppConfig, + appConfigProperties, + "publicCredentialsSaveEnabled", + appConfig.isPublicCredentialsSaveEnabled()); + copyConfigValue( + oldAppConfig, + appConfigProperties, + "adminCredentialsSaveEnabled", + appConfig.isAdminCredentialsSaveEnabled()); + copyConfigValue( + oldAppConfig, appConfigProperties, "enableReverseProxyAuth", appConfig.isEnabledReverseProxyAuth()); + copyConfigValue( + oldAppConfig, appConfigProperties, "forwardProxy", appConfig.isEnabledForwardProxy()); + copyConfigValue( + oldAppConfig, + appConfigProperties, + "linkExternalCredentialsWithUser", + appConfig.isLinkExternalCredentialsWithUser()); + copyConfigValue( + oldAppConfig, appConfigProperties, "redirectOnFederatedAuth", appConfig.isRedirectOnFederatedAuth()); + copyConfigValue( + oldAppConfig, + appConfigProperties, + CBConstants.PARAM_RESOURCE_MANAGER_ENABLED, + appConfig.isResourceManagerEnabled()); + copyConfigValue( + oldAppConfig, + appConfigProperties, + CBConstants.PARAM_SECRET_MANAGER_ENABLED, + appConfig.isSecretManagerEnabled()); + copyConfigValue( + oldAppConfig, + appConfigProperties, + CBConstants.PARAM_SHOW_READ_ONLY_CONN_INFO, + appConfig.isShowReadOnlyConnectionInfo()); + copyConfigValue( + oldAppConfig, + appConfigProperties, + CBConstants.PARAM_CONN_GRANT_ANON_ACCESS, + appConfig.isGrantConnectionsAccessToAnonymousTeam()); + copyConfigValue( + oldAppConfig, + appConfigProperties, + "systemVariablesResolvingEnabled", + appConfig.isSystemVariablesResolvingEnabled() + ); + Map resourceQuotas = new LinkedHashMap<>(); + Map originResourceQuotas = JSONUtils.getObject(oldAppConfig, + CBConstants.PARAM_RESOURCE_QUOTAS); + for (Map.Entry mp : appConfig.getResourceQuotas().entrySet()) { + copyConfigValue(originResourceQuotas, resourceQuotas, mp.getKey(), mp.getValue()); + } + appConfigProperties.put(CBConstants.PARAM_RESOURCE_QUOTAS, resourceQuotas); + + { + // Save only differences in def navigator settings + DBNBrowseSettings navSettings = appConfig.getDefaultNavigatorSettings(); + var navigatorProperties = new LinkedHashMap(); + appConfigProperties.put("defaultNavigatorSettings", navigatorProperties); + + if (navSettings.isShowSystemObjects() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowSystemObjects()) { + navigatorProperties.put("showSystemObjects", navSettings.isShowSystemObjects()); + } + if (navSettings.isShowUtilityObjects() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowUtilityObjects()) { + navigatorProperties.put("showUtilityObjects", navSettings.isShowUtilityObjects()); + } + if (navSettings.isShowOnlyEntities() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowOnlyEntities()) { + navigatorProperties.put("showOnlyEntities", navSettings.isShowOnlyEntities()); + } + if (navSettings.isMergeEntities() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isMergeEntities()) { + navigatorProperties.put("mergeEntities", navSettings.isMergeEntities()); + } + if (navSettings.isHideFolders() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideFolders()) { + navigatorProperties.put("hideFolders", navSettings.isHideFolders()); + } + if (navSettings.isHideSchemas() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideSchemas()) { + navigatorProperties.put("hideSchemas", navSettings.isHideSchemas()); + } + if (navSettings.isHideVirtualModel() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideVirtualModel()) { + navigatorProperties.put("hideVirtualModel", navSettings.isHideVirtualModel()); + } + } + appConfigProperties.put("enabledFeatures", Arrays.asList(appConfig.getEnabledFeatures())); + if (appConfig.getEnabledAuthProviders() != null) { + appConfigProperties.put("enabledAuthProviders", Arrays.asList(appConfig.getEnabledAuthProviders())); + } + appConfigProperties.put("enabledDrivers", Arrays.asList(appConfig.getEnabledDrivers())); + appConfigProperties.put("disabledDrivers", Arrays.asList(appConfig.getDisabledDrivers())); + + if (!CommonUtils.isEmpty(appConfig.getPlugins())) { + appConfigProperties.put("plugins", appConfig.getPlugins()); + } + if (!CommonUtils.isEmpty(appConfig.getAuthCustomConfigurations())) { + appConfigProperties.put("authConfigurations", appConfig.getAuthCustomConfigurations()); + } + } + return rootConfig; + } + + @NotNull + protected Map collectServerConfigProperties( + @NotNull CBServerConfig serverConfig, + Map originServerConfig + ) { + var serverConfigProperties = new LinkedHashMap(); + if (!CommonUtils.isEmpty(serverConfig.getServerName())) { + copyConfigValue(originServerConfig, + serverConfigProperties, + CBConstants.PARAM_SERVER_NAME, + serverConfig.getServerName()); + } + if (!CommonUtils.isEmpty(serverConfig.getServerURL())) { + copyConfigValue( + originServerConfig, serverConfigProperties, CBConstants.PARAM_SERVER_URL, serverConfig.getServerURL()); + } + if (serverConfig.getMaxSessionIdleTime() > 0) { + copyConfigValue( + originServerConfig, + serverConfigProperties, + CBConstants.PARAM_SESSION_EXPIRE_PERIOD, + serverConfig.getMaxSessionIdleTime()); + } + copyConfigValue( + originServerConfig, + serverConfigProperties, + CBConstants.PARAM_FORCE_HTTPS, + serverConfig.isForceHttps() + ); + copyConfigValue( + originServerConfig, + serverConfigProperties, + CBConstants.PARAM_SUPPORTED_HOSTS, + serverConfig.getSupportedHosts() + ); + copyConfigValue( + originServerConfig, + serverConfigProperties, + CBConstants.PARAM_BIND_SESSION_TO_IP, + serverConfig.getBindSessionToIp() + ); + var productConfigProperties = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + Map oldProductRuntimeConfig = JSONUtils.getObject(originServerConfig, + CBConstants.PARAM_PRODUCT_SETTINGS); + Map productSettings = getServerConfiguration().getProductSettings(); + if (!CommonUtils.isEmpty(productSettings)) { + for (Map.Entry mp : productSettings.entrySet()) { + copyConfigValue(oldProductRuntimeConfig, productConfigProperties, mp.getKey(), mp.getValue()); + } + serverConfigProperties.put(CBConstants.PARAM_PRODUCT_SETTINGS, productConfigProperties); + } + return serverConfigProperties; + } + + //////////////////////////////////////////////////////////////////////// + // Configuration utils + + private void patchConfigurationWithProperties(Map configProps) { + IVariableResolver varResolver = new SystemVariablesResolver() { + @Nullable + @Override + public String get(@NotNull String name) { + String propValue = externalProperties.get(name); + if (propValue != null) { + return propValue; + } + return super.get(name); + } + }; + BaseServletApplication.patchConfigurationWithProperties(configProps, varResolver); + } + + // gets info about patterns from original configuration file and saves it to runtime config + protected void copyConfigValue( + Map oldConfig, + Map newConfig, + String key, + Object defaultValue + ) { + //do not store empty values in runtime config + if (defaultValue instanceof String stringValue && CommonUtils.isEmpty(stringValue)) { + return; + } + Object value = oldConfig.get(key); + if (value instanceof Map && defaultValue instanceof Map) { + Map subValue = new LinkedHashMap<>(); + Map oldConfigValue = JSONUtils.getObject(oldConfig, key); + for (Map.Entry entry : oldConfigValue.entrySet()) { + copyConfigValue(oldConfigValue, subValue, entry.getKey(), ((Map) defaultValue).get(entry.getKey())); + } + newConfig.put(key, subValue); + } else { + Object newConfigValue = ServletAppUtils.getExtractedValue(oldConfig.get(key), defaultValue); + newConfig.put(key, newConfigValue); + } + } + + @NotNull + protected Path getRuntimeAppConfigPath() { + return getDataDirectory(true).resolve(CBConstants.RUNTIME_APP_CONFIG_FILE_NAME); + } + + @NotNull + protected Path getRuntimeProductConfigFilePath() { + return getDataDirectory(false).resolve(CBConstants.RUNTIME_PRODUCT_CONFIG_FILE_NAME); + } + + @NotNull + public Path getDataDirectory(boolean create) { + Path dataDir = getWorkspacePath().resolve(CBConstants.RUNTIME_DATA_DIR_NAME); + if (create && !Files.exists(dataDir)) { + try { + Files.createDirectories(dataDir); + } catch (Exception e) { + log.error("Can't create data directory '" + dataDir.toAbsolutePath() + "'"); + } + } + return dataDir; + } + + public void saveProductConfiguration(Map productConfiguration) { + Map productSettings = getServerConfiguration().getProductSettings(); + Map mergedConfig = ServletAppUtils.mergeConfigurations(productSettings, productConfiguration); + productSettings.clear(); + productSettings.putAll(ServletAppUtils.flattenMap(mergedConfig)); + } + + @NotNull + public T getServerConfiguration() { + return serverConfiguration; + } + + public CBAppConfig getAppConfiguration() { + return appConfiguration; + } + + public Map getProductConfiguration() { + return getServerConfiguration().getProductSettings(); + } + + private String readRootUri(String uri) { + //slashes are needed to correctly display static resources on ui + if (!uri.endsWith("/")) { + uri = uri + '/'; + } + if (!uri.startsWith("/")) { + uri = '/' + uri; + } + return uri; + } + + @NotNull + @Override + public Map getOriginalConfigurationProperties() { + return originalConfigurationProperties; + } + + @Override + public void validateFinalServerConfiguration() throws DBException { + + } + + public String getLocalHostAddress() { + return localHostAddress; + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java new file mode 100644 index 0000000000..87d28cd7d1 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java @@ -0,0 +1,104 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import io.cloudbeaver.model.config.CBServerConfig; +import io.cloudbeaver.model.config.WebDatabaseConfig; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.utils.CommonUtils; + +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Server configuration controller for embedded products. + */ +public class CBServerConfigurationControllerEmbedded extends CBServerConfigurationController { + + private static final Log log = Log.getLog(CBServerConfigurationControllerEmbedded.class); + + public CBServerConfigurationControllerEmbedded(@NotNull T serverConfig, @NotNull Path homeDirectory) { + super(serverConfig, homeDirectory); + } + + @NotNull + @Override + protected Map collectServerConfigProperties( + @NotNull CBServerConfig serverConfig, + @NotNull Map originServerConfig + ) { + Map serverConfigProperties = super.collectServerConfigProperties(serverConfig, originServerConfig); + + var databaseConfigProperties = new LinkedHashMap(); + Map oldRuntimeDBConfig = JSONUtils.getObject(originServerConfig, + CBConstants.PARAM_DB_CONFIGURATION); + Gson gson = getGson(); + Map dbConfigMap = gson.fromJson( + gson.toJsonTree(getServerConfiguration().getDatabaseConfiguration()), + JSONUtils.MAP_TYPE_TOKEN + ); + if (!CommonUtils.isEmpty(dbConfigMap)) { + for (Map.Entry mp : dbConfigMap.entrySet()) { + copyConfigValue(oldRuntimeDBConfig, databaseConfigProperties, mp.getKey(), mp.getValue()); + } + serverConfigProperties.put(CBConstants.PARAM_DB_CONFIGURATION, databaseConfigProperties); + } + savePasswordPolicyConfig(originServerConfig, serverConfigProperties); + return serverConfigProperties; + } + + + private void savePasswordPolicyConfig(Map originServerConfig, Map serverConfigProperties) { + // save password policy configuration + var passwordPolicyProperties = new LinkedHashMap(); + + var oldRuntimePasswordPolicyConfig = JSONUtils.getObject( + JSONUtils.getObject(originServerConfig, CBConstants.PARAM_SM_CONFIGURATION), + CBConstants.PARAM_PASSWORD_POLICY_CONFIGURATION + ); + Gson gson = getGson(); + Map passwordPolicyConfig = gson.fromJson( + gson.toJsonTree(getServerConfiguration().getSecurityManagerConfiguration().getPasswordPolicyConfiguration()), + JSONUtils.MAP_TYPE_TOKEN + ); + if (!CommonUtils.isEmpty(passwordPolicyConfig)) { + for (Map.Entry mp : passwordPolicyConfig.entrySet()) { + copyConfigValue(oldRuntimePasswordPolicyConfig, passwordPolicyProperties, mp.getKey(), mp.getValue()); + } + serverConfigProperties.put( + CBConstants.PARAM_SM_CONFIGURATION, + Map.of(CBConstants.PARAM_PASSWORD_POLICY_CONFIGURATION, passwordPolicyProperties) + ); + } + } + + @NotNull + @Override + protected GsonBuilder getGsonBuilder() { + GsonBuilder gsonBuilder = super.getGsonBuilder(); + var databaseConfiguration = getServerConfiguration().getDatabaseConfiguration(); + InstanceCreator dbConfigCreator = type -> databaseConfiguration; + return gsonBuilder + .registerTypeAdapter(WebDatabaseConfig.class, dbConfigCreator); + } +} diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java new file mode 100644 index 0000000000..9b367adbb8 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java @@ -0,0 +1,188 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.WebSessionGlobalProjectImpl; +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.WebAppUtils; +import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.utils.ServletAppUtils; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.security.SMAdminController; +import org.jkiss.dbeaver.model.security.SMObjectPermissionsGrant; +import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; +import org.jkiss.dbeaver.model.websocket.event.permissions.WSObjectPermissionEvent; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class WSObjectPermissionUpdatedEventHandler extends WSDefaultEventHandler { + private static final Log log = Log.getLog(WSObjectPermissionUpdatedEventHandler.class); + + @Override + public void handleEvent(@NotNull WSObjectPermissionEvent event) { + String objectId = event.getObjectId(); + Consumer runnable = switch (event.getSmObjectType()) { + case project: + yield getUpdateUserProjectsInfoConsumer(event, objectId); + case datasource: + try { + SMAdminController smController = CBApplication.getInstance().getSecurityController(); + Set dataSourcePermissions = smController.getObjectPermissionGrants(event.getObjectId(), event.getSmObjectType()) + .stream() + .map(SMObjectPermissionsGrant::getSubjectId).collect(Collectors.toSet()); + yield getUpdateUserDataSourcesInfoConsumer(event, objectId, dataSourcePermissions); + } catch (DBException e) { + log.error("Error getting permissions for data source " + objectId, e); + yield null; + } + }; + if (runnable == null) { + return; + } + log.debug(event.getTopicId() + " event handled"); + Collection allSessions = CBApplication.getInstance().getSessionManager().getAllActiveSessions(); + for (var activeUserSession : allSessions) { + if (!isAcceptableInSession(activeUserSession, event)) { + log.debug("Cannot handle %s event '%s' in session %s".formatted( + event.getTopicId(), + event.getId(), + activeUserSession.getSessionId() + )); + continue; + } + log.debug("%s event '%s' handled".formatted(event.getTopicId(), event.getId())); + runnable.accept(activeUserSession); + } + } + + @NotNull + private Consumer getUpdateUserDataSourcesInfoConsumer( + @NotNull WSObjectPermissionEvent event, + @NotNull String dataSourceId, + @NotNull Set dataSourcePermissions + ) { + return (activeUserSession) -> { + // we have accessible data sources only in web session + // admins already have access for all shared connections + if (!(activeUserSession instanceof WebSession webSession) || SMUtils.isAdmin(webSession)) { + return; + } + if (!isAcceptableInSession(webSession, event)) { + return; + } + var user = activeUserSession.getUserContext().getUser(); + Set userSubjects = new HashSet<>(); + if (user == null) { + String anonymousUserTeam = WebAppUtils.getWebApplication().getAppConfiguration().getAnonymousUserTeam(); + if (anonymousUserTeam == null) { + // cannot apply event for anonymous user is there are no user subjects + return; + } + userSubjects.add(anonymousUserTeam); + } else { + userSubjects.addAll(Set.of(user.getTeams())); + userSubjects.add(user.getUserId()); + } + boolean shouldBeAccessible = dataSourcePermissions.stream().anyMatch(userSubjects::contains); + List dataSources = List.of(dataSourceId); + WebSessionGlobalProjectImpl project = webSession.getGlobalProject(); + if (project == null) { + log.error("Project " + ServletAppUtils.getGlobalProjectId() + + " is not found in session " + activeUserSession.getSessionId()); + return; + } + boolean isAccessibleNow = project.findWebConnectionInfo(dataSourceId) != null; + if (WSObjectPermissionEvent.UPDATED.equals(event.getId())) { + if (isAccessibleNow || !shouldBeAccessible) { + return; + } + project.addAccessibleConnectionToCache(dataSourceId); + webSession.addSessionEvent( + WSDataSourceEvent.create( + event.getSessionId(), + event.getUserId(), + project.getId(), + dataSources, + WSDataSourceProperty.CONFIGURATION + ) + ); + } else if (WSObjectPermissionEvent.DELETED.equals(event.getId())) { + if (!isAccessibleNow || shouldBeAccessible) { + return; + } + project.removeAccessibleConnectionFromCache(dataSourceId); + webSession.addSessionEvent( + WSDataSourceEvent.delete( + event.getSessionId(), + event.getUserId(), + project.getId(), + dataSources, + WSDataSourceProperty.CONFIGURATION + ) + ); + } + }; + } + + @NotNull + private Consumer getUpdateUserProjectsInfoConsumer( + @NotNull WSObjectPermissionEvent event, + @NotNull String projectId + ) { + return (activeUserSession) -> { + try { + if (WSObjectPermissionEvent.UPDATED.equals(event.getId())) { + var accessibleProjectIds = activeUserSession.getUserContext().getAccessibleProjectIds(); + if (accessibleProjectIds.contains(event.getObjectId())) { + return; + } + activeUserSession.addSessionProject(projectId); + activeUserSession.addSessionEvent( + WSProjectUpdateEvent.create( + event.getSessionId(), + event.getUserId(), + projectId + ) + ); + } else if (WSObjectPermissionEvent.DELETED.equals(event.getId())) { + activeUserSession.removeSessionProject(projectId); + activeUserSession.addSessionEvent( + WSProjectUpdateEvent.delete( + event.getSessionId(), + event.getUserId(), + projectId + ) + ); + } + } catch (DBException e) { + log.error("Error on changing permissions for project " + + event.getObjectId() + " in session " + activeUserSession.getSessionId(), e); + } + }; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java similarity index 78% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java rename to server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java index 098bbbe8eb..130558de14 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -25,13 +25,8 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.rm.RMEvent; import org.jkiss.dbeaver.model.rm.RMEventManager; -import org.jkiss.dbeaver.model.rm.RMResource; -import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceUpdatedEvent; -import java.util.Arrays; -import java.util.List; - /** * Notify all active user session that rm resource has been updated */ @@ -45,7 +40,7 @@ protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @Not if (activeUserSession instanceof WebSession) { var webSession = (WebSession) activeUserSession; acceptChangesInNavigatorTree( - WSEventType.valueById(event.getId()), + event.getId(), event.getResourcePath(), webSession.getProjectById(event.getProjectId()) ); @@ -53,17 +48,17 @@ protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @Not activeUserSession.addSessionEvent(event); } - private void acceptChangesInNavigatorTree(WSEventType eventType, String resourcePath, WebProjectImpl project) { - if (eventType == WSEventType.RM_RESOURCE_CREATED) { + private void acceptChangesInNavigatorTree(@NotNull String eventId, String resourcePath, WebProjectImpl project) { + if (WSResourceUpdatedEvent.CREATED.equals(eventId)) { RMEventManager.fireEvent( new RMEvent(RMEvent.Action.RESOURCE_ADD, - project.getRmProject(), + project.getRMProject(), resourcePath) ); - } else if (eventType == WSEventType.RM_RESOURCE_DELETED) { + } else if (WSResourceUpdatedEvent.DELETED.equals(eventId)) { RMEventManager.fireEvent( new RMEvent(RMEvent.Action.RESOURCE_DELETE, - project.getRmProject(), + project.getRMProject(), resourcePath) ); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSSubjectPermissionUpdatedEventHandler.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSSubjectPermissionUpdatedEventHandler.java similarity index 82% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSSubjectPermissionUpdatedEventHandler.java rename to server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSSubjectPermissionUpdatedEventHandler.java index d71b7131ae..8af5037199 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSSubjectPermissionUpdatedEventHandler.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSSubjectPermissionUpdatedEventHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,6 +17,7 @@ package io.cloudbeaver.server.events; import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebHeadlessSession; import io.cloudbeaver.service.security.SMUtils; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; @@ -41,7 +42,8 @@ protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @Not } activeUserSession.refreshUserData(); var newUserPermissions = activeUserSession.getUserContext().getUserPermissions(); - boolean shouldUpdateData = !(SMUtils.isRMAdmin(oldUserPermissions) && SMUtils.isRMAdmin(newUserPermissions)); + boolean shouldUpdateData = activeUserSession instanceof WebHeadlessSession + || !(SMUtils.isRMAdmin(oldUserPermissions) && SMUtils.isRMAdmin(newUserPermissions)); if (shouldUpdateData) { super.updateSessionData(activeUserSession, event); } @@ -52,18 +54,15 @@ protected boolean isAcceptableInSession(@NotNull BaseWebSession activeUserSessio if (!super.isAcceptableInSession(activeUserSession, event)) { return false; } + var user = activeUserSession.getUserContext().getUser(); if (user == null) { return false; } var subjectId = event.getSubjectId(); - switch (event.getSubjectType()) { - case user: - return CommonUtils.equalObjects(user.getUserId(), subjectId); - case team: - return ArrayUtils.containsIgnoreCase(user.getTeams(), subjectId); - default: - return false; - } + return switch (event.getSubjectType()) { + case user -> CommonUtils.equalObjects(user.getUserId(), subjectId); + case team -> ArrayUtils.containsIgnoreCase(user.getTeams(), subjectId); + }; } } diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSUserEventHandler.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSUserEventHandler.java new file mode 100644 index 0000000000..9b68c14f2d --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSUserEventHandler.java @@ -0,0 +1,45 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.CBApplication; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.websocket.WSEventHandler; +import org.jkiss.dbeaver.model.websocket.event.WSAbstractEvent; +import org.jkiss.dbeaver.model.websocket.event.WSUserCloseSessionsEvent; +import org.jkiss.dbeaver.model.websocket.event.WSUserDeletedEvent; +import org.jkiss.dbeaver.model.websocket.event.WSUserDisabledEvent; + +public class WSUserEventHandler implements WSEventHandler { + @Override + public void handleEvent(@NotNull EVENT event) { + var sessionManager = CBApplication.getInstance().getSessionManager(); + + switch (event) { + case WSUserCloseSessionsEvent closeSessionsEvent -> { + if (closeSessionsEvent.getSessionIds().isEmpty()) { + sessionManager.closeAllSessions(closeSessionsEvent.getSessionId()); + } else { + sessionManager.closeSessions(closeSessionsEvent.getSessionIds()); + } + } + case WSUserDeletedEvent e -> sessionManager.closeUserSession(e); + case WSUserDisabledEvent e -> sessionManager.closeUserSession(e); + default -> { } + } + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java similarity index 82% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java rename to server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java index 5e84aa20da..438a1ef858 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,6 +16,7 @@ */ package io.cloudbeaver.server.events; +import io.cloudbeaver.WebSessionProjectImpl; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; @@ -36,11 +37,16 @@ public class WSUserSecretEventHandlerImpl extends WSDefaultEventHandler application; + private Server server; + + public CBJettyServer(@NotNull CBApplication application) { + this.application = application; + } + + public void runServer() { + try { + CBServerConfig serverConfiguration = application.getServerConfiguration(); + int serverPort = serverConfiguration.getServerPort(); + String serverHost = serverConfiguration.getServerHost(); + Path sslPath = getSslConfigurationPath(); + + boolean sslConfigurationExists = sslPath != null && Files.exists(sslPath); + if (sslConfigurationExists) { + server = new Server(); + XmlConfiguration sslConfiguration = new XmlConfiguration(ResourceFactory.of(server).newResource(sslPath)); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + // method sslConfiguration.configure() does not see the context class of the Loader, + // so we have to configure it manually, then return the old classLoader. + Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); + sslConfiguration.configure(server); + Thread.currentThread().setContextClassLoader(classLoader); + } else { + if (CommonUtils.isEmpty(serverHost)) { + server = new Server(serverPort); + } else { + server = new Server( + InetSocketAddress.createUnresolved(serverHost, serverPort)); + } + } + + { + + // Handler configuration + Path contentRootPath = Path.of(serverConfiguration.getContentRoot()); + ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); + servletContextHandler.setBaseResourceAsPath(contentRootPath); + String rootURI = serverConfiguration.getRootURI(); + servletContextHandler.setContextPath(rootURI); + + + ServletHolder staticServletHolder = new ServletHolder( + "static", new CBStaticServlet(Path.of(serverConfiguration.getContentRoot())) + ); + staticServletHolder.setInitParameter("dirAllowed", "false"); + staticServletHolder.setInitParameter("cacheControl", + "public, max-age=" + CBConstants.STATIC_CACHE_SECONDS); + servletContextHandler.addServlet(staticServletHolder, "/"); + + if (Files.isSymbolicLink(contentRootPath)) { + servletContextHandler.addAliasCheck(new CBSymLinkContentAllowedAliasChecker(contentRootPath)); + } + + ServletHolder imagesServletHolder = new ServletHolder("images", new CBImageServlet()); + servletContextHandler.addServlet(imagesServletHolder, serverConfiguration.getServicesURI() + "images/*"); + + servletContextHandler.addServlet(new ServletHolder("status", new WebStatusServlet()), "/status"); + + GraphQLEndpoint endpoint = new GraphQLEndpoint(new ServerConfigurationTimeLimitFilter(application)); + application.addApplicationContextValue(GraphQL.class.getName(), endpoint.getGraphQL()); + String gqlServletPath = serverConfiguration.getServicesURI() + "gql/*"; + servletContextHandler.addServlet( + new ServletHolder( + "graphql", + endpoint + ), + gqlServletPath + ); + servletContextHandler.addEventListener(new CBServerContextListener(application)); + + // Add extensions from services + Set excludedFilterPaths = new HashSet<>(); + CBJettyServletContext servletContext = new CBJettyServletContext(servletContextHandler); + for (DBWServiceBindingServlet wsd : WebServiceRegistry.getInstance() + .getWebServices(DBWServiceBindingServlet.class) + ) { + if (wsd.isApplicable(this.application)) { + try { + wsd.addServlets(this.application, servletContext); + excludedFilterPaths.addAll(wsd.getExcludedServletPaths(this.application)); + } catch (DBException e) { + log.error(e.getMessage(), e); + } + } + } + + CBJettyWebSocketContext webSocketContext = new CBJettyWebSocketContext(server, servletContextHandler); + for (DBWServiceBindingWebSocket wsb : WebServiceRegistry.getInstance() + .getWebServices(DBWServiceBindingWebSocket.class) + ) { + if (wsb.isApplicable(this.application)) { + try { + wsb.addWebSockets(this.application, webSocketContext); + } catch (DBException e) { + log.error(e.getMessage(), e); + } + } + } + FilterHolder hostsFilter = new FilterHolder(new RequestHostFilter( + application, + excludedFilterPaths, + Set.of(gqlServletPath) + )); + servletContextHandler.addFilter(hostsFilter, "/*", null); + + + JakartaWebSocketServletContainerInitializer.configure(servletContextHandler, (context, container) -> { + // Add echo endpoint to server container + ServerEndpointConfig eventWsEnpoint = ServerEndpointConfig.Builder + .create( + CBEventsWebSocket.class, + serverConfiguration.getServicesURI() + "ws" + ).configurator(new CBWebSocketServerConfigurator(application.getSessionManager())) + .build(); + container.addEndpoint(eventWsEnpoint); + }); + + JettyUtils.initSessionManager( + this.application.getMaxSessionIdleTime(), + this.application, + server, + servletContextHandler + ); + + server.setHandler(servletContextHandler); + + ErrorPageErrorHandler errorHandler = new ErrorPageErrorHandler(); + //errorHandler.addErrorPage(404, "/missing.html"); + servletContextHandler.setErrorHandler(errorHandler); + + log.debug("Active servlets:"); //$NON-NLS-1$ + for (ServletMapping sm : servletContextHandler.getServletHandler().getServletMappings()) { + log.debug("\t" + sm.getServletName() + ": " + Arrays.toString(sm.getPathSpecs())); //$NON-NLS-1$ + } + + log.debug("Active websocket mappings:"); + for (String mapping : webSocketContext.getMappings()) { + log.debug("\t" + mapping); + } + + } + + boolean forwardProxy = application.getAppConfiguration().isEnabledForwardProxy(); + { + // HTTP config + for(Connector y : server.getConnectors()) { + for(ConnectionFactory x : y.getConnectionFactories()) { + if(x instanceof HttpConnectionFactory) { + HttpConfiguration httpConfiguration = ((HttpConnectionFactory)x).getHttpConfiguration(); + httpConfiguration.setSendServerVersion(false); + if (forwardProxy) { + httpConfiguration.addCustomizer(new ForwardedRequestCustomizer()); + } + } + } + } + } + refreshJettyConfig(); + server.start(); + server.join(); + } catch (Exception e) { + log.error("Error running Jetty server", e); + } + } + + @Nullable + private Path getSslConfigurationPath() { + var sslConfigurationPath = application.getServerConfiguration().getSslConfigurationPath(); + if (sslConfigurationPath == null) { + return null; + } + var sslConfiguration = Path.of(sslConfigurationPath); + return sslConfiguration.isAbsolute() ? sslConfiguration : application.getHomeDirectory().resolve(sslConfiguration); + } + + public synchronized void refreshJettyConfig() { + if (server == null) { + return; + } + log.info("Refreshing Jetty configuration"); + if (server.getHandler() instanceof ServletContextHandler servletContextHandler + && servletContextHandler.getSessionHandler() instanceof CBSessionHandler cbSessionHandler + ) { + cbSessionHandler.setMaxCookieAge((int) (application.getMaxSessionIdleTime() / 1000)); + cbSessionHandler.setSecureCookies(application.getServerConfiguration().isForceHttps()); + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/CBServerContextListener.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/CBServerContextListener.java new file mode 100644 index 0000000000..af6215b0ff --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/CBServerContextListener.java @@ -0,0 +1,50 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.jetty; + +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBConstants; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.SessionCookieConfig; + +public class CBServerContextListener implements ServletContextListener { + + // One week + //private static final int CB_SESSION_LIFE_TIME = 60 * 60 * 24 * 7; + private final CBApplication application; + + public CBServerContextListener(CBApplication application) { + this.application = application; + } + + public void contextInitialized(ServletContextEvent sce) { + SessionCookieConfig cookieConfig = sce.getServletContext().getSessionCookieConfig(); + + cookieConfig.setComment("Cloudbeaver Session ID"); + //scf.setDomain(domain); + //scf.setMaxAge(CB_SESSION_LIFE_TIME); + cookieConfig.setPath(CBApplication.getInstance().getRootURI()); +// cookieConfig.setSecure(application.getServerURL().startsWith("https")); + cookieConfig.setHttpOnly(true); + cookieConfig.setName(CBConstants.CB_SESSION_COOKIE_NAME); + } + + public void contextDestroyed(ServletContextEvent sce) { + + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/RequestHostFilter.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/RequestHostFilter.java new file mode 100644 index 0000000000..abdcb13a8c --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/RequestHostFilter.java @@ -0,0 +1,184 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.jetty; + +import com.google.common.net.InetAddresses; +import io.cloudbeaver.model.config.CBServerConfig; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.utils.ServletAppUtils; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.utils.CommonUtils; + +import java.io.IOException; +import java.net.URI; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class RequestHostFilter implements Filter { + private static final Log log = Log.getLog(RequestHostFilter.class); + + @NotNull + private final CBApplication application; + private final Set excludedPaths = new HashSet<>(); + private final Set errorPaths = new HashSet<>(); + + public RequestHostFilter( + @NotNull CBApplication application, + @NotNull Set excludedPaths, + @NotNull Set errorPaths + ) { + this.application = application; + this.excludedPaths.addAll( + excludedPaths.stream() + .map(path -> ServletAppUtils.removeSideSlashes(path.replace("*", ""))) + .toList() + ); + this.errorPaths.addAll( + errorPaths.stream() + .map(path -> ServletAppUtils.removeSideSlashes(path.replace("*", ""))) + .toList() + ); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + boolean requestAllowed = true; + + if (request instanceof HttpServletRequest httpRequest) { + CBServerConfig serverConfig = application.getServerConfiguration(); + URI originUri; + try { + String origin = ServletAppUtils.getOriginFromRequest(httpRequest); + originUri = URI.create(origin); + } catch (Exception e) { + log.error("Failed to get origin from request", e); + chain.doFilter(request, response); + return; + } + boolean isIpAddress = InetAddresses.isInetAddress(originUri.getHost()); + String servletPath = httpRequest.getServletPath(); + requestAllowed = isIpAddress; + if (!requestAllowed) { + if (CommonUtils.isNotEmpty(servletPath)) { + for (String excludedPath : excludedPaths) { + if (servletPath.contains(excludedPath)) { + chain.doFilter(request, response); + return; + } + } + } + requestAllowed = validateHosts(serverConfig, httpRequest, (HttpServletResponse) response, originUri); + } + if (requestAllowed) { + requestAllowed = validateSchema(serverConfig, httpRequest, (HttpServletResponse) response, originUri); + } + } + if (requestAllowed) { + chain.doFilter(request, response); + } + } + + private boolean validateSchema( + @NotNull CBServerConfig serverConfig, + @NotNull HttpServletRequest httpRequest, + @NotNull HttpServletResponse response, + @NotNull URI originUri + ) { + boolean httpsExpected = serverConfig.isForceHttps(); + try { + if ("http".equals(originUri.getScheme()) && httpsExpected) { + log.warn("Request schema is 'http' but 'forceHttps' is enabled. Redirecting to 'https'."); + StringBuilder redirectUrlBuilder = new StringBuilder("https://") + .append(originUri.getHost()); + if (originUri.getPort() > -1) { + redirectUrlBuilder.append(':').append(originUri.getPort()); + } + redirectUrlBuilder.append(httpRequest.getRequestURI()); + if (httpRequest.getQueryString() != null) { + redirectUrlBuilder.append("?") + .append(httpRequest.getQueryString()); + } + response.sendRedirect(redirectUrlBuilder.toString()); + return false; + } + } catch (Exception e) { + log.error("Failed to redirect to HTTPS", e); + } + return true; + } + + private boolean validateHosts( + @NotNull CBServerConfig serverConfig, + @NotNull HttpServletRequest httpRequest, + @NotNull HttpServletResponse response, + URI originUri + ) throws IOException { + List availableHosts = serverConfig.getSupportedHosts(); + if (CommonUtils.isEmpty(availableHosts)) { + return true; + } + try { + var requestHostBuilder = new StringBuilder(originUri.getHost()); + if (originUri.getPort() > -1) { + requestHostBuilder.append(':').append(originUri.getPort()); + } + String requestHost = requestHostBuilder.toString(); + if (!availableHosts.contains(requestHost)) { + for (String errorPath : errorPaths) { + if (httpRequest.getServletPath().contains(errorPath)) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.getWriter().write( + "Request host '" + requestHost + "' is not allowed. Available hosts: " + availableHosts + ); + return false; + } + } + log.warn("Request host '" + requestHost + "' is not allowed. Redirect to default: " + availableHosts); + redirectToDefaultHost(response, httpRequest, availableHosts); + return false; + } + } catch (Throwable e) { + log.error(e.getMessage(), e); + redirectToDefaultHost(response, httpRequest, availableHosts); + return false; + } + return true; + } + + private void redirectToDefaultHost( + @NotNull HttpServletResponse response, + @NotNull HttpServletRequest httpRequest, + @NotNull List availableHosts + ) throws IOException { + boolean https = application.getServerConfiguration().isForceHttps(); + String redirectUrl = (https ? "https://" : "http://") + getDefaultHost(availableHosts) + httpRequest.getRequestURI(); + if (httpRequest.getQueryString() != null) { + redirectUrl += "?" + httpRequest.getQueryString(); + } + response.sendRedirect(redirectUrl); + } + + @NotNull + private String getDefaultHost(@NotNull List availableHosts) { + return availableHosts.getFirst(); + } +} diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jobs/SessionStateJob.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jobs/SessionStateJob.java new file mode 100644 index 0000000000..3625ddfdcd --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jobs/SessionStateJob.java @@ -0,0 +1,45 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.jobs; + +import io.cloudbeaver.service.session.CBSessionManager; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.app.DBPPlatform; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.PeriodicJob; + +import java.time.Duration; + +public class SessionStateJob extends PeriodicJob { + private static final Log log = Log.getLog(SessionStateJob.class); + private final CBSessionManager sessionManager; + + public SessionStateJob(@NotNull DBPPlatform platform, CBSessionManager sessionManager) { + super("Session state sender", platform, Duration.ofSeconds(30)); + this.sessionManager = sessionManager; + } + + @Override + protected void doJob(@NotNull DBRProgressMonitor monitor) { + try { + sessionManager.sendSessionsStates(); + } catch (Exception e) { + log.error("Error sending session state", e); + } + } +} diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java new file mode 100644 index 0000000000..23718a58e6 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java @@ -0,0 +1,48 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.jobs; + +import io.cloudbeaver.service.session.CBSessionManager; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.app.DBPPlatform; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.PeriodicJob; + +import java.time.Duration; + +/** + * WebSessionMonitorJob + */ +public class WebSessionMonitorJob extends PeriodicJob { + private static final Log log = Log.getLog(WebSessionMonitorJob.class); + private final CBSessionManager sessionManager; + + public WebSessionMonitorJob(@NotNull DBPPlatform platform, @NotNull CBSessionManager sessionManager) { + super("Web session monitor", platform, Duration.ofSeconds(10)); + this.sessionManager = sessionManager; + } + + @Override + protected void doJob(@NotNull DBRProgressMonitor monitor) { + try { + sessionManager.expireIdleSessions(); + } catch (Exception e) { + log.error("Error on expire idle sessions", e); + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/servlets/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/servlets/CBStaticServlet.java new file mode 100644 index 0000000000..9f2dd3a04b --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/servlets/CBStaticServlet.java @@ -0,0 +1,241 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.servlets; + +import io.cloudbeaver.DBWConstants; +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.auth.CBAuthConstants; +import io.cloudbeaver.auth.SMAuthProviderFederated; +import io.cloudbeaver.model.config.CBAppConfig; +import io.cloudbeaver.model.config.CBServerConfig; +import io.cloudbeaver.model.session.WebActionParameters; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.registry.WebAuthProviderDescriptor; +import io.cloudbeaver.registry.WebAuthProviderRegistry; +import io.cloudbeaver.registry.WebHandlerRegistry; +import io.cloudbeaver.registry.WebServletHandlerDescriptor; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBPlatform; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.eclipse.jetty.http.HttpHeader; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.auth.SMAuthInfo; +import org.jkiss.dbeaver.model.auth.SMAuthProvider; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; +import org.jkiss.dbeaver.utils.MimeTypes; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +@WebServlet(urlPatterns = "/") +public class CBStaticServlet extends DefaultServlet { + private static final String AUTO_LOGIN_ACTION = "auto-login"; + private static final String AUTO_LOGIN_AUTH_ID = "auth-id"; + private static final String ACTION = "action"; + + private static final Log log = Log.getLog(CBStaticServlet.class); + + @NotNull + private final Path contentRoot; + + public CBStaticServlet(@NotNull Path contentRoot) { + this.contentRoot = contentRoot; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + for (WebServletHandlerDescriptor handler : WebHandlerRegistry.getInstance().getServletHandlers()) { + try { + if (handler.getInstance().handleRequest(this, request, response)) { + return; + } + } catch (DBException e) { + log.warn("Servlet handler '" + handler.getId() + "' failed", e); + } + } + String uri = request.getPathInfo(); + try { + CBApplication cbApplication = CBPlatform.getInstance().getApplication(); + finishSessionLoginIfNeeded(request, response); + if (cbApplication.getAppConfiguration().isRedirectOnFederatedAuth() + && isRootServiceUri(uri) + && request.getParameterMap().isEmpty() + ) { + if (performAutoLogin(request, response)) { + return; + } + } + } catch (DBWebException e) { + log.error("Error reading websession", e); + } + patchStaticContentIfNeeded(request, response); + } + + private static boolean isRootServiceUri(String uri) { + return CommonUtils.isEmpty(uri) || uri.equals("/") || uri.equals("/index.html"); + } + + private void finishSessionLoginIfNeeded(HttpServletRequest request, HttpServletResponse response) throws DBWebException { + boolean isAutoLogin = CommonUtils.toBoolean(request.getParameter(CBAuthConstants.CB_AUTO_LOGIN_REQUEST_PARAM)); + if (!isAutoLogin) { + return; + } + + WebSession webSession = CBApplication.getInstance().getSessionManager().getWebSession( + request, response, false); + if (webSession.getUserContext().isNonAnonymousUserAuthorizedInSM()) { + log.warn("Auto login failed: user already authorized"); + return; + } + + String authId = request.getParameter(CBAuthConstants.CB_AUTH_ID_REQUEST_PARAM); + if (CommonUtils.isEmpty(authId)) { + log.warn("Auto login failed: authId not found in request"); + return; + } + Map authActionParams = Map.of( + ACTION, AUTO_LOGIN_ACTION, + AUTO_LOGIN_AUTH_ID, authId + ); + WebActionParameters.saveToSession(webSession, authActionParams); + } + + private boolean performAutoLogin(HttpServletRequest request, HttpServletResponse response) { + CBApplication application = CBApplication.getInstance(); + if (application.isConfigurationMode()) { + return false; + } + CBAppConfig appConfig = application.getAppConfiguration(); + String[] authProviders = appConfig.getEnabledAuthProviders(); + if (authProviders.length == 1) { + String authProviderId = authProviders[0]; + WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(authProviderId); + if (authProvider != null && authProvider.isConfigurable()) { + SMAuthProviderCustomConfiguration activeAuthConfig = null; + for (SMAuthProviderCustomConfiguration cfg : appConfig.getAuthCustomConfigurations()) { + if (!cfg.isDisabled() && cfg.getProvider().equals(authProviderId)) { + if (activeAuthConfig != null) { + return false; + } + activeAuthConfig = cfg; + } + } + if (activeAuthConfig == null) { + return false; + } + + try { + WebSession webSession = CBApplication.getInstance().getSessionManager().getWebSession( + request, response, false); + WebActionParameters webActionParameters = WebActionParameters.fromSession(webSession, false); + if (webActionParameters != null && webActionParameters.getParameters().containsValue(AUTO_LOGIN_ACTION)) { + return false; + } + // We have the only provider + // Forward to signon URL + SMAuthProvider authProviderInstance = authProvider.getInstance(); + if (authProviderInstance instanceof SMAuthProviderFederated) { + if (webSession.getUser() == null) { + var securityController = webSession.getSecurityController(); + SMAuthInfo authInfo = securityController.authenticate( + webSession.getSessionId(), + null, + webSession.getSessionParameters(), + WebSession.CB_SESSION_TYPE, + authProvider.getId(), + activeAuthConfig.getId(), + Map.of(), + false + ); + String signInLink = authInfo.getRedirectUrl(); + //ignore current routing if non-root page is open + if (!signInLink.endsWith("#")) { + signInLink += "#"; + } + if (!CommonUtils.isEmpty(signInLink)) { + // Redirect to it + Map authActionParams = Map.of( + ACTION, AUTO_LOGIN_ACTION, + AUTO_LOGIN_AUTH_ID, authInfo.getAuthAttemptId() + ); + WebActionParameters.saveToSession(webSession, authActionParams); + request.getSession().setAttribute(DBWConstants.STATE_ATTR_SIGN_IN_STATE, DBWConstants.SignInState.GLOBAL); + response.sendRedirect(signInLink); + return true; + } + } + } + } catch (Exception e) { + log.debug("Error reading auth provider configuration", e); + } + } + } + + return false; + } + + private void patchStaticContentIfNeeded(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String pathInContext = request.getServletPath(); + + if ("/".equals(pathInContext)) { + pathInContext = "index.html"; + } + + if (pathInContext == null || !pathInContext.endsWith("index.html") + && !pathInContext.endsWith("sso.html") + && !pathInContext.endsWith("ssoError.html") + ) { + super.doGet(request, response); + return; + } + + if (pathInContext.startsWith("/")) { + pathInContext = pathInContext.substring(1); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Path filePath = contentRoot.resolve(pathInContext); + try (InputStream fis = Files.newInputStream(filePath)) { + IOUtils.copyStream(fis, baos); + } + String indexContents = baos.toString(StandardCharsets.UTF_8); + CBServerConfig serverConfig = CBApplication.getInstance().getServerConfiguration(); + indexContents = indexContents + .replace("{ROOT_URI}", serverConfig.getRootURI()) + .replace("{STATIC_CONTENT}", serverConfig.getStaticContent()); + byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); + + // Disable cache for index.html + response.setHeader(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); + response.setHeader(HttpHeader.CONTENT_TYPE.toString(), MimeTypes.TEXT_HTML); + response.setHeader(HttpHeader.EXPIRES.toString(), "0"); + response.getOutputStream().write(indexBytes); + } + +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceInitializer.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/DBWServiceInitializer.java similarity index 81% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceInitializer.java rename to server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/DBWServiceInitializer.java index ea9ebf7aee..23bf4529fc 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceInitializer.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/DBWServiceInitializer.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,14 +16,14 @@ */ package io.cloudbeaver.service; +import io.cloudbeaver.server.CBApplication; import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.app.DBPApplication; /** * Web service implementation */ public interface DBWServiceInitializer extends DBWServiceBinding { - void initializeService(DBPApplication application) throws DBException; + void initializeService(CBApplication application) throws DBException; } diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/core/CECoreModelExtender.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/core/CECoreModelExtender.java new file mode 100644 index 0000000000..4b5bf1f11d --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/core/CECoreModelExtender.java @@ -0,0 +1,35 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.core; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.service.DBWBindingContext; +import io.cloudbeaver.service.WebServiceBindingBase; + +/** + * extends the base gql model, to avoid unnecessary fields in other applications + */ +public class CECoreModelExtender extends WebServiceBindingBase { + public CECoreModelExtender() { + super(DBWVoidService.class, new DBWVoidService(), "schema/service.core.graphqls"); + } + + @Override + public void bindWiring(DBWBindingContext model) throws DBWebException { + + } +} diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/core/DBWVoidService.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/core/DBWVoidService.java new file mode 100644 index 0000000000..852b182519 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/core/DBWVoidService.java @@ -0,0 +1,22 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.core; + +import io.cloudbeaver.service.DBWService; + +public class DBWVoidService implements DBWService { +} diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java new file mode 100644 index 0000000000..07623cbc5f --- /dev/null +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java @@ -0,0 +1,469 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.session; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.auth.SMTokenCredentialProvider; +import io.cloudbeaver.model.session.*; +import io.cloudbeaver.registry.WebHandlerRegistry; +import io.cloudbeaver.registry.WebSessionHandlerDescriptor; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBConstants; +import io.cloudbeaver.server.WebAppSessionManager; +import io.cloudbeaver.server.events.WSWebUtils; +import io.cloudbeaver.service.DBWSessionHandler; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.auth.SMAuthInfo; +import org.jkiss.dbeaver.model.security.user.SMAuthPermissions; +import org.jkiss.dbeaver.model.websocket.event.WSAbstractEvent; +import org.jkiss.dbeaver.model.websocket.event.WSUserDeletedEvent; +import org.jkiss.dbeaver.model.websocket.event.WSUserDisabledEvent; +import org.jkiss.dbeaver.model.websocket.event.session.WSSessionStateEvent; +import org.jkiss.utils.CommonUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Web session manager + */ +public class CBSessionManager implements WebAppSessionManager { + + private static final Log log = Log.getLog(CBSessionManager.class); + + private final CBApplication application; + private final Map sessionMap = new HashMap<>(); + + public CBSessionManager(CBApplication application) { + this.application = application; + } + + /** + * Closes Web Session, associated to HttpSession from {@code request} + */ + @Override + public BaseWebSession closeSession(@NotNull HttpServletRequest request) { + HttpSession session = request.getSession(); + if (session != null) { + return closeSession(getSessionId(request)); + } + return null; + } + + @Override + public BaseWebSession closeSession(@NotNull String sessionId) { + BaseWebSession webSession; + synchronized (sessionMap) { + webSession = sessionMap.remove(sessionId); + } + if (webSession != null) { + log.debug("> Close session '" + sessionId + "'"); + webSession.close(); + return webSession; + } + return null; + } + + protected CBApplication getApplication() { + return application; + } + + @Deprecated + public boolean touchSession( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response + ) throws DBWebException { + WebSession webSession = getWebSession(request, response, false); + var requestInfo = new WebHttpRequestInfo(request); + webSession.updateSessionParameters(requestInfo); + webSession.updateInfo(!request.getSession().isNew()); + return true; + } + + @Override + @NotNull + public WebSession getWebSession( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response + ) throws DBWebException { + return getWebSession(request, response, true); + } + + @Override + @NotNull + public WebSession getWebSession( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + boolean errorOnNoFound + ) throws DBWebException { + String sessionId = getSessionId(request); + WebSession webSession; + synchronized (sessionMap) { + var baseWebSession = sessionMap.get(sessionId); + if (baseWebSession == null && CBApplication.getInstance().isConfigurationMode()) { + try { + webSession = createWebSessionImpl(new WebHttpRequestInfo(request)); + } catch (DBException e) { + throw new DBWebException("Failed to create web session", e); + } + sessionMap.put(sessionId, webSession); + } else if (baseWebSession == null) { + try { + webSession = createWebSessionImpl(new WebHttpRequestInfo(request)); + } catch (DBException e) { + throw new DBWebException("Failed to create web session", e); + } + + boolean restored = false; + try { + restored = restorePreviousUserSession(webSession); + } catch (DBException e) { + log.error("Failed to restore previous user session", e); + } + + HttpSession httpSession = request.getSession(false); + if (!restored && errorOnNoFound && !httpSession.isNew()) { + throw new DBWebException("Session has expired", DBWebException.ERROR_CODE_SESSION_EXPIRED); + } + + log.debug((restored ? "Restored " : "New ") + "web session '" + webSession.getSessionId() + "'"); + + webSession.setCacheExpired(!httpSession.isNew()); + + sessionMap.put(sessionId, webSession); + } else { + if (!(baseWebSession instanceof WebSession)) { + throw new DBWebException("Unexpected session type: " + baseWebSession.getClass().getName()); + } + webSession = (WebSession) baseWebSession; + } + } + + validateSessionIp(request, webSession); + + return webSession; + } + + private void validateSessionIp(@NotNull HttpServletRequest request, WebSession webSession) { + boolean bindingEnabled = isSessionBindingEnabled(request); + String currentRemote = request.getRemoteAddr(); + if (bindingEnabled + && (CommonUtils.isEmpty(currentRemote) || !currentRemote.equals(webSession.getLastRemoteAddr())) + ) { + var error = new DBWebException( + "Session remote address mismatch. Expected: " + webSession.getLastRemoteAddr() + + ", actual: " + currentRemote, + DBWebException.ERROR_CODE_ACCESS_DENIED + ); + log.error(error); + webSession.addSessionError(error); + closeSession(webSession.getSessionId()); + } + } + + protected boolean isSessionBindingEnabled(@NotNull HttpServletRequest request) { + String bindingState = application.getServerConfiguration().getBindSessionToIp(); + return CBConstants.BIND_SESSION_ENABLE.equalsIgnoreCase(bindingState) || Boolean.parseBoolean(bindingState); + } + + @NotNull + protected String getSessionId(@NotNull HttpServletRequest request) { + HttpSession httpSession = request.getSession(true); + return httpSession.getId(); + } + + /** + * Returns not expired session from cache, or restore it. + * + * @return WebSession object or null, if session expired or invalid + */ + @Nullable + public WebSession getOrRestoreWebSession(@NotNull WebHttpRequestInfo requestInfo) { + final var sessionId = requestInfo.getId(); + if (sessionId == null) { + log.debug("Http session is null. No Web Session returned"); + return null; + } + WebSession webSession; + synchronized (sessionMap) { + if (sessionMap.containsKey(sessionId)) { + var cachedWebSession = sessionMap.get(sessionId); + if (!(cachedWebSession instanceof WebSession)) { + log.warn("Unexpected session type: " + cachedWebSession.getClass().getName()); + return null; + } + return (WebSession) cachedWebSession; + } else { + try { + var oldAuthInfo = getApplication().getSecurityController().restoreUserSession(sessionId); + if (oldAuthInfo == null) { + log.debug("Couldn't restore previous user session '" + sessionId + "'"); + return null; + } + + webSession = createWebSessionImpl(requestInfo); + restorePreviousUserSession(webSession, oldAuthInfo); + + sessionMap.put(sessionId, webSession); + log.debug("Web session restored"); + return webSession; + } catch (DBException e) { + log.error("Failed to restore previous user session", e); + return null; + } + } + } + } + + private boolean restorePreviousUserSession(@NotNull WebSession webSession) throws DBException { + var oldAuthInfo = webSession.getSecurityController().restoreUserSession(webSession.getSessionId()); + if (oldAuthInfo == null) { + return false; + } + + restorePreviousUserSession(webSession, oldAuthInfo); + return true; + } + + private void restorePreviousUserSession( + @NotNull WebSession webSession, + @NotNull SMAuthInfo authInfo + ) throws DBException { + var linkWithActiveUser = false; // because its old credentials and should already be linked if needed + new WebSessionAuthProcessor(webSession, authInfo, linkWithActiveUser) + .authenticateSession(); + } + + @NotNull + protected WebSession createWebSessionImpl(@NotNull WebHttpRequestInfo request) throws DBException { + return new WebSession(request, application, getSessionHandlers()); + } + + @NotNull + protected Map getSessionHandlers() { + return WebHandlerRegistry.getInstance().getSessionHandlers() + .stream() + .collect(Collectors.toMap(WebSessionHandlerDescriptor::getId, WebSessionHandlerDescriptor::getInstance)); + } + + @Override + @Nullable + public BaseWebSession getSession(@NotNull String sessionId) { + synchronized (sessionMap) { + return sessionMap.get(sessionId); + } + } + + @Override + @Nullable + public WebSession findWebSession(HttpServletRequest request) { + String sessionId = getSessionId(request); + synchronized (sessionMap) { + var session = sessionMap.get(sessionId); + if (session instanceof WebSession) { + return (WebSession) session; + } + return null; + } + } + + @Override + public WebSession findWebSession(HttpServletRequest request, boolean errorOnNoFound) throws DBWebException { + WebSession webSession = findWebSession(request); + if (webSession != null) { + return webSession; + } + if (errorOnNoFound) { + throw new DBWebException("Session has expired", DBWebException.ERROR_CODE_SESSION_EXPIRED); + } + return null; + } + + public void expireIdleSessions() { + long maxSessionIdleTime = application.getMaxSessionIdleTime(); + + List expiredList = new ArrayList<>(); + synchronized (sessionMap) { + for (Iterator iterator = sessionMap.values().iterator(); iterator.hasNext(); ) { + var session = iterator.next(); + long idleMillis = System.currentTimeMillis() - session.getLastAccessTimeMillis(); + if (idleMillis >= maxSessionIdleTime) { + iterator.remove(); + expiredList.add(session); + } + } + } + + for (var session : expiredList) { + closeExpiredSession(session); + } + } + + @Override + public Collection getAllActiveSessions() { + synchronized (sessionMap) { + return new ArrayList<>(sessionMap.values()); + } + } + + @Nullable + public WebHeadlessSession getHeadlessSession( + @Nullable String smAccessToken, + @NotNull WebHttpRequestInfo requestInfo, + boolean create + ) throws DBException { + if (CommonUtils.isEmpty(smAccessToken)) { + return null; + } + synchronized (sessionMap) { + var tempCredProvider = new SMTokenCredentialProvider(smAccessToken); + SMAuthPermissions authPermissions = application.createSecurityController(tempCredProvider).getTokenPermissions(); + var sessionId = requestInfo.getId() != null ? requestInfo.getId() + : authPermissions.getSessionId(); + + var existSession = sessionMap.get(sessionId); + + if (existSession instanceof WebHeadlessSession) { + var creds = existSession.getUserContext().getActiveUserCredentials(); + if (creds == null || !smAccessToken.equals(creds.getSmAccessToken())) { + existSession.getUserContext().refresh( + smAccessToken, + null, + authPermissions + ); + } + return (WebHeadlessSession) existSession; + } + if (existSession != null) { + //session exist but it not headless session + return null; + } + if (!create) { + return null; + } + var headlessSession = new WebHeadlessSession( + sessionId, + application + ); + headlessSession.getUserContext().refresh( + smAccessToken, + null, + authPermissions + ); + sessionMap.put(sessionId, headlessSession); + return headlessSession; + } + } + + /** + * Send session state with remaining alive time to all cached session + */ + public void sendSessionsStates() { + synchronized (sessionMap) { + sessionMap.values() + .parallelStream() + .filter(session -> { + if (session instanceof WebSession webSession) { + return webSession.isAuthorizedInSecurityManager(); + } + return false; + }) + .forEach(session -> { + try { + session.addSessionEvent(new WSSessionStateEvent( + session.getLastAccessTimeMillis(), + session.getRemainingTime(), + session.isValid(), + ((WebSession) session).isCacheExpired(), + ((WebSession) session).getLocale(), + ((WebSession) session).getActionParameters())); + } catch (Exception e) { + log.error("Failed to refresh session state: " + session.getSessionId(), e); + } + }); + } + } + + public void closeUserSession(@NotNull WSAbstractEvent event) { + synchronized (sessionMap) { + for (Iterator iterator = sessionMap.values().iterator(); iterator.hasNext(); ) { + var session = iterator.next(); + if (CommonUtils.equalObjects(session.getUserContext().getUserId(), + event.getUserId())) { + if (session instanceof WebHeadlessSession headlessSession) { + headlessSession.addSessionEvent(event); + } + iterator.remove(); + session.close(); + } + } + } + } + + public void closeSessions(@NotNull List smSessionsId) { + synchronized (sessionMap) { + for (Iterator iterator = sessionMap.values().iterator(); iterator.hasNext(); ) { + var session = iterator.next(); + if (smSessionsId.contains(session.getUserContext().getSmSessionId())) { + iterator.remove(); + session.close(false, true); + } + } + } + } + + /** + * Closes all sessions in session manager. + */ + public void closeAllSessions(@Nullable String initiatorSessionId) { + synchronized (sessionMap) { + for (Iterator iterator = sessionMap.values().iterator(); iterator.hasNext(); ) { + var session = iterator.next(); + iterator.remove(); + session.close(false, !WSWebUtils.isSessionIdEquals(session, initiatorSessionId)); + } + } + } + + /** + * Creates new web session or returns existing one. + */ + public WebSession createWebSession(WebHttpRequestInfo requestInfo) throws DBException { + String id = requestInfo.getId(); + synchronized (sessionMap) { + BaseWebSession baseWebSession = sessionMap.get(id); + if (baseWebSession instanceof WebSession) { + return (WebSession) baseWebSession; + } else { + WebSession webSessionImpl = createWebSessionImpl(requestInfo); + sessionMap.put(id, webSessionImpl); + return webSessionImpl; + } + } + } + + protected void closeExpiredSession(@NotNull BaseWebSession session) { + log.debug("> Expire session '" + session.getSessionId() + "'"); + session.close(); + } +} diff --git a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF index 2acb2838ed..aa6b9b6f2e 100644 --- a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF @@ -3,35 +3,38 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Server Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true -Bundle-Version: 23.2.2.qualifier -Bundle-Release-Date: 20230209 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 25.2.1.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy -Bundle-Activator: io.cloudbeaver.server.CBPlatformActivator -Bundle-ClassPath: . Require-Bundle: org.eclipse.core.runtime;visibility:=reexport, - org.eclipse.core.resources;visibility:=reexport, org.apache.commons.jexl, org.jkiss.utils;visibility:=reexport, org.jkiss.dbeaver.model.sql;visibility:=reexport, org.jkiss.dbeaver.data.gis, org.jkiss.bundle.jetty.server;visibility:=reexport, com.google.gson;visibility:=reexport, - org.jkiss.bundle.graphql.java;visibility:=reexport, + org.jkiss.bundle.graphql;visibility:=reexport, org.jkiss.bundle.apache.dbcp, org.jkiss.dbeaver.net.ssh, io.cloudbeaver.model;visibility:=reexport, io.cloudbeaver.service.security;visibility:=reexport Export-Package: io.cloudbeaver, io.cloudbeaver.model, + io.cloudbeaver.model.app, io.cloudbeaver.model.user, io.cloudbeaver.registry, + io.cloudbeaver.server.events, io.cloudbeaver.server, io.cloudbeaver.server.actions, + io.cloudbeaver.server.jetty, + io.cloudbeaver.server.graphql, + io.cloudbeaver.server.jobs, io.cloudbeaver.server.servlets, + io.cloudbeaver.server.websockets, io.cloudbeaver.service, io.cloudbeaver.service.navigator, - io.cloudbeaver.service.session, io.cloudbeaver.service.sql Import-Package: org.slf4j +Bundle-Localization: OSGI-INF/l10n/bundle Automatic-Module-Name: io.cloudbeaver.server diff --git a/server/bundles/io.cloudbeaver.server/OSGI-INF/l10n/bundle.properties b/server/bundles/io.cloudbeaver.server/OSGI-INF/l10n/bundle.properties new file mode 100644 index 0000000000..3d208419e0 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/OSGI-INF/l10n/bundle.properties @@ -0,0 +1,2 @@ +log.api.graphql.debug = Log GraphQL requests to console +log.api.graphql.debug.description = Enable detailed logging of GraphQL queries in the server log, including all provided variables diff --git a/server/bundles/io.cloudbeaver.server/OSGI-INF/l10n/bundle_ru.properties b/server/bundles/io.cloudbeaver.server/OSGI-INF/l10n/bundle_ru.properties new file mode 100644 index 0000000000..ddb9a9c941 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/OSGI-INF/l10n/bundle_ru.properties @@ -0,0 +1,2 @@ +log.api.graphql.debug =Логировать GraphQL-запросы в консоль +log.api.graphql.debug.description = Включите подробное логирование запросов GraphQL в журнале сервера, включая все предоставленные переменные diff --git a/server/bundles/io.cloudbeaver.server/build.properties b/server/bundles/io.cloudbeaver.server/build.properties index 0e7161380c..6728418a75 100644 --- a/server/bundles/io.cloudbeaver.server/build.properties +++ b/server/bundles/io.cloudbeaver.server/build.properties @@ -2,6 +2,7 @@ source.. = src/ output.. = target/classes/ bin.includes = .,\ META-INF/,\ + OSGI-INF/,\ schema/,\ static/,\ plugin.xml diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index eb75b6ef1d..fa0738a7aa 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -5,20 +5,17 @@ - - - - - - - - + + + + + @@ -37,9 +34,6 @@ - - - @@ -47,7 +41,6 @@ - @@ -55,24 +48,30 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/server/bundles/io.cloudbeaver.server/pom.xml b/server/bundles/io.cloudbeaver.server/pom.xml index b4ba87ffe9..df1546c9cb 100644 --- a/server/bundles/io.cloudbeaver.server/pom.xml +++ b/server/bundles/io.cloudbeaver.server/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.server - 23.2.2-SNAPSHOT + 25.2.1-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.server/schema/io.cloudbeaver.object.feature.provider.exsd b/server/bundles/io.cloudbeaver.server/schema/io.cloudbeaver.object.feature.provider.exsd new file mode 100644 index 0000000000..5dba048b86 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/schema/io.cloudbeaver.object.feature.provider.exsd @@ -0,0 +1,34 @@ + + + + + + + + Web feature + + + + + + + + + + + + + + + + + + + + Web service description + + + + + + diff --git a/server/bundles/io.cloudbeaver.server/schema/schema.graphqls b/server/bundles/io.cloudbeaver.server/schema/schema.graphqls index afffe35fef..191dc13f01 100644 --- a/server/bundles/io.cloudbeaver.server/schema/schema.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/schema.graphqls @@ -2,13 +2,14 @@ scalar Object # Date/Time scalar DateTime +scalar Date input PageInput { limit: Int 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..42066205ea 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -3,247 +3,397 @@ # General stuff #################################################### -# Property - +""" +Information about the object property used to generate its UI +""" type ObjectPropertyInfo { - # ID + "Unique property identifier" id: String - # Human readable name + "Human-readable name" displayName: String - # Property description + "Property description" description: String - # Property category (may be used if object has a lot of properties) + "Usage hint for the property" + 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) + "Property data type (e.g., int, String)" dataType: String - - # Property value. Note: for some properties value reading may take a lot of time (e.g. RowCount for tables) + "Property value (can be resource-intensive for some properties, e.g., RowCount for tables)" value: Object - - # List of values this property can take. Makes sense only for enumerable properties + "List of allowed values (for enumerable properties)" validValues: [ Object ] - # Default property value + "Default property value" defaultValue: Object - - # Property value length + "Property value length" length: ObjectPropertyLength! - - # Supported features (system, hidden, inherited, foreign, expensive, etc) + "List of supported features (e.g., system, hidden, inherited, foreign, expensive)" features: [ String! ]! - # Order position + "Order position" order: Int! - # Supported configuration types (for driver properties) + "Supported configuration types (for driver properties)" supportedConfigurationTypes: [ String! ] + "Is the property required" + required: Boolean! @since(version: "23.3.1") + "List of preference scopes (e.g., global, user)" + scopes: [String!] + "Dynamic conditions for the property (e.g., visibility or read-only)" + conditions: [Condition!] @since(version: "25.0.1") } enum ObjectPropertyLength { - # 1 character + "1 character" TINY, - # 20 characters + "20 characters" SHORT, - # <= 64 characters + "<= 64 characters" MEDIUM, - # Full line length. The default + "Full line length. The default" LONG, - # Multi-line long text + "Multi-line long text" MULTILINE } -# Async types +"Represents a dynamic condition for a property, such as visibility or read-only state" +type Condition @since(version: "25.0.1") { + "The logical expression that defines when the condition applies" + expression: String! + "The type of condition (e.g., HIDE or READ_ONLY)" + conditionType: ConditionType! +} + +enum ConditionType @since(version: "25.0.1") { + "hiding property condition" + HIDE, + "restriction for setting a property value" + READ_ONLY +} +""" +Async types +""" type AsyncTaskInfo { + "Task unique identifier" id: String! + "Async task name" name: String + "Indicates if the task is currently running" running: Boolean! + "Current status of the async task" status: String + "Error information if the task failed" error: ServerError - result: SQLExecuteInfo @deprecated # Deprecated. Use asyncSqlExecuteResults instead - # Task result. - # Can be some kind of identifier to obtain real result using another API function + """ + Task result. + Can be some kind of identifier to obtain real result using another API function + """ taskResult: Object } -# Various server errors descriptor +"Various server errors descriptor" type ServerError { + "Error message text" message: String + "Retrieves the vendor-specific error code" errorCode: String + "Type/category of the error" errorType: String + "Stack trace for debugging" stackTrace: String + "Nested error that caused this error (recursive)" causedBy: ServerError } type ServerMessage { + "The time when the server message was created" time: String + "The content of the message" message: String } -# Languages supported by server - +"Languages supported by server" type ServerLanguage { + "ISO 639-1 or similar language code (e.g., \"en\", \"ru\")" isoCode: String! + "Display name of the language in the current locale (e.g., \"English\")" displayName: String + "Native name of the language (e.g., \"English\", \"Русский\")" nativeName: String } -type WebServiceConfig { - id: String! - name: String! - description: String! - bundleVersion: String! +"Password policy configuration" +type PasswordPolicyConfig @since(version: "23.3.3") { + "Minimum password length" + minLength: Int! + "Minimum number of digits required" + minNumberCount: Int! + "Minimum number of symbols required" + minSymbolCount: Int! + "Require both uppercase and lowercase letters" + requireMixedCase: Boolean! } +"Product information" type ProductInfo { + "ID of the product" id: ID! + "The product version" version: String! + "The product name" name: String! + "The product description" description: String + "The build timestamp of the product" buildTime: String! + "The release timestamp of the product" releaseTime: String! + "Information about the product license" licenseInfo: String + "Information about the latest available version" latestVersionInfo: String + "URL for purchasing the product" + productPurchaseURL: String } +"Server configuration" type ServerConfig { + "Server name" name: String! + "Version of the server" version: String! + "ID of the server workspace" workspaceId: ID! - serverURL: String! - rootURI: String! - hostName: String! @deprecated # use container id instead - containerId: String! - defaultAuthRole: String - defaultUserTeam: String # [23.2.2] - + "Defines if the anonymous access is enabled" anonymousAccessEnabled: Boolean! + "Defines if non-admin users can create connections" supportsCustomConnections: Boolean! - supportsConnectionBrowser: Boolean! - supportsWorkspaces: Boolean! + "Defines if resource manager is enabled" resourceManagerEnabled: Boolean! + "Defines if secret manager is enabled" + secretManagerEnabled: Boolean! @since(version: "24.3.2") + + "Defines is it is possible to save user database credentials" publicCredentialsSaveEnabled: Boolean! + "Defines is it is possible to save global database credentials" adminCredentialsSaveEnabled: Boolean! + "Defines if the server requires a license" licenseRequired: Boolean! + "Defines if the server license is valid" licenseValid: Boolean! + "Returns information about the server license status" + licenseStatus: String @since(version: "24.1.5") - sessionExpireTime: Int! - localHostAddress: String - + "Defines if the server is in configuration mode" configurationMode: Boolean! + "Defines if the server is in development mode" developmentMode: Boolean! - redirectOnFederatedAuth: Boolean! + "Defines if the server is distributed" distributed: Boolean! + "List of enabled features" enabledFeatures: [ID!]! - enabledAuthProviders: [ID!]! + "List of disabled beta features" + disabledBetaFeatures: [ID!] @since(version: "24.0.5") + "List of server features" + serverFeatures: [ID!] @since(version: "24.3.0") + "List of supported languages" supportedLanguages: [ ServerLanguage! ]! - services: [ WebServiceConfig ] + "Product configuration" productConfiguration: Object! + "Product information" productInfo: ProductInfo! + "Navigator settings for the server" defaultNavigatorSettings: NavigatorSettings! + "List of disabled drivers (IDs of DriverInfo)" disabledDrivers: [ID!]! + "Resource quotas (e.g., max amount of running SQL queries)" resourceQuotas: Object! } +type ProductSettingsGroup @since(version: "24.0.1") { + id: ID! + displayName: String! +} + +type ProductSettings @since(version: "24.0.1") { + groups: [ProductSettingsGroup!]! + "each property is associated with a group by category" + settings: [ObjectPropertyInfo!]! +} + type SessionInfo { + "The time when the session was created" createTime: String! + "The last time the session was accessed" lastAccessTime: String! + "The current locale of the session" locale: String! + "Indicates whether the session cache has expired" cacheExpired: Boolean! - serverMessages: [ ServerMessage ] + "List of active connections in the session" connections: [ ConnectionInfo! ]! + "Action parameters for the session (e.g., opening a connection)" actionParameters: Object + "Indicates if the session is valid" valid: Boolean! + "Remaining time before the session expires (in seconds)" remainingTime: Int! } #################################################### -# Drivers and connections +"Drivers and connections" #################################################### type DatabaseAuthModel { + "Auth model unique ID" id: ID! + "Display name of the auth model" displayName: String! + "Description of the auth model" description: String + "Path to the auth model icon" icon: String - # checks if the auth model needs a configuration on a local file system + "Checks if the auth model needs a configuration on a local file system" requiresLocalConfiguration: Boolean + "Returns id of the required auth provider if the auth model requires it" requiredAuth: String + "List of properties for the auth model that can be displayed in the UI" properties: [ObjectPropertyInfo!]! } type DriverInfo { + """ + Driver unique full ID. It is `providerId + "." + driverId`. + It is recommended to use providerId and driverId separately. + """ id: ID! + "Name of the driver" name: String + "Description of the driver" description: String + "Path to the driver icon" icon: String + "Path to the driver icon for big size" iconBig: String - # Driver provider ID - providerId: ID - # Driver Java class name + "Driver ID. It is unique within provider" + driverId: ID! + "Driver provider ID. It is globally unique" + providerId: ID! + "Driver Java class name" driverClassName: String + "Default host for the driver" defaultHost: String + "Default port for the driver" defaultPort: String + "Default database name for the driver" defaultDatabase: String + "Default server name for the driver" defaultServer: String + "Default user name for the driver" defaultUser: String + "Default connection URL for the driver" sampleURL: String + "Returns link to the driver documentation page" driverInfoURL: String + "Returns link to the driver properties page" driverPropertiesURL: String + "Defines if the database for this driver is embedded" embedded: Boolean + "Defines if the driver is enabled" enabled: Boolean! - requiresServerName: Boolean - requiresDatabaseName: Boolean - + "Defines if the driver page requires server name field" + requiresServerName: Boolean @deprecated(reason: "use mainProperties instead") + "Defines if the driver page requires database name field" + requiresDatabaseName: Boolean @deprecated(reason: "use mainProperties instead") + "Defines if host, port, database, server name fields are using a custom page" + useCustomPage: Boolean! @since(version: "24.1.2") + + "Defines if driver license is required" licenseRequired: Boolean + "Driver license information" license: String + "Defines if the driver is a custom driver" custom: Boolean - # Driver score for ordering, biggest first + "Driver score for ordering, biggest first" promotedScore: Int - # Never used? - #connectionProperties: Object - #defaultConnectionProperties: Object - - # Driver properties. - # Note: it is expensive property and it may produce database server roundtrips. - # Call it only when you really need it. - # These properties are for advanced users in usually shouldn't be specified for new connections. + """ + Driver properties. + Note: it is expensive property and it may produce database server roundtrips. + Call it only when you really need it. + These properties are for advanced users in usually shouldn't be specified for new connections. + """ driverProperties: [ObjectPropertyInfo!]! - # Driver parameters (map name->value) + "Driver parameters (map name->value)" driverParameters: Object! - # Additional driver provider properties - # These properties can be configured by user on main connection page - # to provide important connection settings + """ + Main driver properties. + Contains info about main fields (host, port, database, server name) that are used in main connection page + """ + mainProperties: [ObjectPropertyInfo!]! @since(version: "24.1.2") + + """ + Additional driver provider properties. + These properties can be configured by user on main connection page to provide important connection settings + """ providerProperties: [ObjectPropertyInfo!]! - # False for drivers which do not support authentication - anonymousAccess: Boolean + "Expert driver settings properties. Returns properties (like keep-alive interval) that are not often used and can be hidden in UI" + expertSettingsProperties: [ObjectPropertyInfo!]! @since(version: "25.2.1") + "False for drivers which do not support authentication." + anonymousAccess: Boolean + "Default auth model that is used for this driver (see authModels)" defaultAuthModel: ID! + "List of auth models that can be used with this driver (see authModels)" applicableAuthModels: [ID!]! - + "List of network handlers that can be used with this driver (SSH/SSL)" applicableNetworkHandlers: [ID]! - + "Configuration types are used in UI to determine how to display connection settings (show host/port/database fields or use URL field)" configurationTypes: [DriverConfigurationType]! + "Defines if the driver can be downloaded remotely" + downloadable: Boolean! @since(version: "24.3.3") + "Defines if the driver is installed on the server" + driverInstalled: Boolean! + "List of driver libraries that are used for connecting to the database" driverLibraries: [DriverLibraryInfo!]! + "Defines if embedded driver is safe to use in the server" + safeEmbeddedDriver: Boolean! @since(version: "25.0.0") } +"Driver library information. Used to display driver files in UI" type DriverLibraryInfo { + "Driver library unique ID" id: ID! + "Driver library name" name: String! + "Path to the driver library icon" + icon: String + "List of files that are used by the driver" + libraryFiles: [DriverFileInfo!] +} + +"Driver file information." +type DriverFileInfo @since(version: "24.3.2") { + "Driver file unique ID" + id: ID! + "Driver file name" + fileName: String! + "Path to the driver file icon" icon: String } @@ -255,7 +405,9 @@ enum ResultDataFormat { } enum DriverConfigurationType { + "Driver uses host, port, database and server name fields" MANUAL, + "Driver uses URL field" URL } @@ -267,12 +419,17 @@ enum NetworkHandlerType { CONFIG } +"SSH network handler authentication type" enum NetworkHandlerAuthType { PASSWORD, PUBLIC_KEY, AGENT } +""" +Network handler descriptor. +This descriptor is used to describe network handlers (SSH/SSL) that can be used for connections. +""" type NetworkHandlerDescriptor { id: ID! codeName: String! @@ -280,28 +437,47 @@ type NetworkHandlerDescriptor { description: String secured: Boolean! type: NetworkHandlerType + "Properties that can be displayed in the UI" properties: [ObjectPropertyInfo!]! } -# SSH network handler config. Name without prefix only for backward compatibility +""" +SSH/SSL network handler config. Name without prefix only for backward compatibility +""" type NetworkHandlerConfig { id: ID! + "Defines if the network handler is enabled" enabled: Boolean! - authType: NetworkHandlerAuthType! @deprecated # use properties + "SSH network handler auth type" + authType: NetworkHandlerAuthType! @deprecated(reason: "use properties") + "SSH network handler user name" userName: String + "SSH network handler user password" password: String - key: String @deprecated # use secured properties + "SSH network handler private key" + key: String @deprecated(reason: "use secured properties") + "A flag that indicates if the password should be saved in the secure storage" savePassword: Boolean! + "Network handler properties (name/value)" properties: Object! + "Network handler secure properties (name/value). Used for passwords and keys" secureProperties: Object! } -# Connection instance +type SecretInfo { + displayName: String! + secretId: String! +} + +"Connection instance" type ConnectionInfo { + "Connection unique ID" id: ID! + "ID of the driver that is used for this connection (see DriverInfo)" driverId: ID! - + "Connection name" name: String! + "Connection description" description: String host: String @@ -310,43 +486,67 @@ type ConnectionInfo { databaseName: String url: String - properties: Object + "Main connection properties. Contains host, port, database, server name fields" + mainPropertyValues: Object @since(version: "24.1.2") + + "Expert connection settings. Contains expert settings properties (like keep-alive interval or auto-commit) that are not often used" + expertSettingsValues: Object @since(version: "25.2.1") - template: Boolean! + "Connection keep-alive interval in seconds" + keepAliveInterval: Int! + "Defines if the connection is in auto-commit mode" + autocommit: Boolean + + properties: Object + "Indicates if the connection is already connected to the database" connected: Boolean! provided: Boolean! + "Indicates if the connection is read-only (no data modification allowed)" readOnly: Boolean! - # Forces connection URL use, host/port/database parameters will be ignored + "Forces connection URL use, host/port/database parameters will be ignored" useUrl: Boolean! - # Forces credentials save. This flag doesn't work in shared projects. + "Forces credentials save. This flag doesn't work in shared projects." saveCredentials: Boolean! - # Shared credentials - the same for all users, stored in secure storage. + "Shared credentials - the same for all users, stored in secure storage." sharedCredentials: Boolean! - # Determines that credentials were saved for current user. - # This field read is slow, it should be read only when it really needed + + sharedSecrets: [SecretInfo!]! @since(version: "23.3.5") + """ + Determines that credentials were saved for current user. + This field read is slow, it should be read only when it really needed + """ credentialsSaved: Boolean! - # Determines that additional credentials are needed to connect - # This field read is slow, it should be read only when it really needed + """ + Determines that additional credentials are needed to connect + This field read is slow, it should be read only when it really needed + """ authNeeded: Boolean! + "ID of the connection folder where this connection is stored" folder: ID + "Node path of the connection in the navigator" nodePath: String + "Connection time in ISO format" connectTime: String + "Connection error if any" connectionError: ServerError + "Server version that is used for this connection" serverVersion: String + "Client version that is used for this connection" clientVersion: String origin: ObjectOrigin! + "ID of the auth model that is used for this connection (see authModels)" authModel: ID authProperties: [ObjectPropertyInfo!]! providerProperties: Object! networkHandlersConfig: [NetworkHandlerConfig!]! - # Supported features (provided etc) + "Supported features (provided etc)" features: [ String! ]! navigatorSettings: NavigatorSettings! supportedDataFormats: [ ResultDataFormat! ]! @@ -359,6 +559,11 @@ type ConnectionInfo { projectId: ID! requiredAuth: String + defaultCatalogName: String @since(version: "25.0.5") + defaultSchemaName: String @since(version: "25.0.5") + + "List of tools that can be used with this connection. Returns empty list if no tools are available" + tools: [String!]! @since(version: "24.1.3") } type ConnectionFolderInfo { @@ -392,6 +597,14 @@ type NavigatorSettings { hideVirtualModel: Boolean! } +type RMResourceType { + id: String! + displayName: String! + icon: String + fileExtensions: [String!]! + rootFolder: String +} + type ProjectInfo { id: String! global: Boolean! @@ -428,73 +641,98 @@ input NavigatorSettingsInput { input NetworkHandlerConfigInput { id: ID! + "Defines if the network handler should be enabled" enabled: Boolean - authType: NetworkHandlerAuthType @deprecated # use properties + "Network handler type (TUNNEL, PROXY, CONFIG)" + authType: NetworkHandlerAuthType @deprecated (reason: "use properties") + "Sets user name for the network handler (SSH)" userName: String + "Sets user password for the network handler (SSH)" password: String - key: String @deprecated # use secured properties + "Sets private key for the network handler (SSH)" + key: String @deprecated(reason: "use secured properties") + "Sets a flag that indicates if the password should be saved in the secure storage" savePassword: Boolean + "Network handler properties (name/value)" properties: Object + "Network handler secure properties (name/value). Used for passwords and keys" secureProperties: Object } -# Configuration of particular connection. Used for new connection create. Includes auth info +"Configuration of particular connection. Used for new connection create. Includes auth info" input ConnectionConfig { - # used only for testing created connection + "used only for testing created connection" connectionId: String name: String description: String - # ID of template connection - templateId: ID - # ID of database driver + "ID of database driver" driverId: ID # Custom connection parameters (all optional) - host: String port: String serverName: String databaseName: String - # Connection url jdbc:{driver}://{host}[:{port}]/[{database}] + + "Host, port, serverName, databaseName are also stored in mainPropertyValues for custom pages" + mainPropertyValues: Object @since(version: "24.1.2") + + "Keep-alive, auto-commit, read-only and other expert settings are stored in expertSettingsValues" + expertSettingsValues: Object @since(version: "25.2.1") + + "Sets connection URL jdbc:{driver}://{host}[:{port}]/[{database}]" url: String - # Properties + + "Set properties list" properties: Object - # Template connection - template: Boolean - # Read-onyl connection - readOnly: Boolean + "Set keep-alive interval" + keepAliveInterval: Int @deprecated(reason: "25.2.1 use expertPropertyValues instead") + + "Sets auto-commit connection state" + autocommit: Boolean @deprecated(reason: "25.2.1 use expertPropertyValues instead") + + "Sets read-only connection state" + readOnly: Boolean @deprecated(reason: "25.2.1 use expertPropertyValues instead") # User credentials + "Flag for saving credentials in secure storage" saveCredentials: Boolean + "Flag for using shared credentials." sharedCredentials: Boolean + "Auth model ID that will be used for connection" authModelId: ID + "Secret ID that will be used for connection" + selectedSecretId: ID @since(version: "23.3.5") + "Credentials for the connection (usually user name and password but it may vary for different auth models)" credentials: Object - # Map of provider properties (name/value) - + "Returns map of provider properties (name/value)" providerProperties: Object - # Network handlers. Map of id->property map (name/value). - + "Returns network handlers configuration. Map of id->property map (name/value)." networkHandlersConfig: [NetworkHandlerConfigInput!] #### deprecated fields - # ID of predefined datasource - dataSourceId: ID #@deprecated + "ID of predefined datasource" + dataSourceId: ID @deprecated - # Direct user credentials - userName: String #@deprecated - userPassword: String #@deprecated + "Direct user credentials" + userName: String @deprecated(reason: "use credentials") + userPassword: String @deprecated(reason: "use credentials") - # Folder + "Defines in which connection folder the connection should be created" folder: ID - # Configuration type + "Configuration type (MANUAL, URL)" configurationType: DriverConfigurationType + "Sets catalog name for the connection" + defaultCatalogName: String @since(version: "25.0.5") @deprecated(reason: "25.2.1 use expertPropertyValues instead") + "Sets schema name for the connection" + defaultSchemaName: String @since(version: "25.0.5") @deprecated(reason: "25.2.1 use expertPropertyValues instead") } #################################################### @@ -502,90 +740,108 @@ input ConnectionConfig { #################################################### extend type Query { - # Returns server config + "Returns server config" serverConfig: ServerConfig! + "Returns server system information properties" + systemInfo: [ObjectPropertyInfo!]! @since(version: "24.3.5") - # Returns session state ( initialize if not ) + "Returns product settings" + productSettings: ProductSettings! @since(version: "24.0.1") + + "Returns session state ( initialize if not )" sessionState: SessionInfo! - # Session permissions + "Returns session permissions" sessionPermissions: [ID]! - # Get driver info + "Returns list of available drivers" driverList( id: ID ): [ DriverInfo! ]! + "Returns list of available database auth models" authModels: [DatabaseAuthModel!]! + "Returns list of available network handlers" networkHandlers: [NetworkHandlerDescriptor!]! - # List of user connections. + "Returns list of user connections" userConnections( projectId: ID, id: ID, projectIds: [ID!] ): [ ConnectionInfo! ]! - # List of template connections. - templateConnections( projectId: ID ): [ ConnectionInfo! ]! - # List of connection folders + "Returns list of connection folders" connectionFolders( projectId: ID, path: ID ): [ ConnectionFolderInfo! ]! - # Return connection info + "Returns connection info" connectionInfo( projectId: ID!, id: ID! ): ConnectionInfo! - # Return list of accessible user projects + "Returns list of accessible user projects" listProjects: [ ProjectInfo! ]! + "Reads session log entries" readSessionLog(maxEntries: Int, clearEntries: Boolean): [ LogEntry! ]! } extend type Mutation { - # Initialize session + "Initialize session" openSession(defaultLocale: String): SessionInfo! - # Destroy session + "Destroy session" closeSession: Boolean - # Refreshes session on server and returns its state - touchSession: Boolean + "Refreshes session on server and returns its state" + touchSession: Boolean @deprecated(reason: "use events to update session") + "Refreshes session on server and returns session state" + updateSession: SessionInfo! @since(version: "24.0.0") @deprecated(reason: "use events to update session") - # Refresh session connection list + "Refresh session connection list" refreshSessionConnections: Boolean - # Refreshes session on server and returns its state + "Change session language to specified" changeSessionLanguage(locale: String): Boolean - # Create new custom connection. Custom connections exist only within the current session. + "Create new custom connection" createConnection( config: ConnectionConfig!, projectId: ID ): ConnectionInfo! + "Update specified connection" updateConnection( config: ConnectionConfig!, projectId: ID ): ConnectionInfo! + "Delete specified connection" deleteConnection( id: ID!, projectId: ID ): Boolean! - createConnectionFromTemplate( templateId: ID!, projectId: ID!, connectionName: String ): ConnectionInfo! - - # Create new folder + "Create new folder for connections" createConnectionFolder(parentFolderPath: ID, folderName: String!, projectId: ID ): ConnectionFolderInfo! + "Delete specified connection folder" deleteConnectionFolder( folderPath: ID!, projectId: ID ): Boolean! - # Copies connection configuration from node + "Copies connection configuration from node" copyConnectionFromNode( nodePath: String!, config: ConnectionConfig, projectId: ID ): ConnectionInfo! - # Test connection configuration. Returns remote server version - testConnection( config: ConnectionConfig!, projectId: ID ): ConnectionInfo! + "Test connection configuration. Returns remote server version" + testConnection( config: ConnectionConfig!, projectId: ID): ConnectionInfo! - # Test connection configuration. Returns remote server version + "Test network handler" testNetworkHandler( config: NetworkHandlerConfigInput! ): NetworkEndpointInfo! - # Initiate existing connection - initConnection( id: ID!, projectId: ID, credentials: Object, networkCredentials: [NetworkHandlerConfigInput!], - saveCredentials:Boolean, sharedCredentials: Boolean ): ConnectionInfo! - - # Disconnect from database + "Initiate existing connection" + initConnection( + id: ID!, + projectId: ID, + credentials: Object, + networkCredentials: [NetworkHandlerConfigInput!], + saveCredentials:Boolean, + sharedCredentials: Boolean, + selectedSecretId:String + ): ConnectionInfo! + + "Disconnect from database" closeConnection( id: ID!, projectId: ID ): ConnectionInfo! - # Changes navigator settings for connection + "Change navigator settings for connection" setConnectionNavigatorSettings( id: ID!, projectId: ID, settings: NavigatorSettingsInput!): ConnectionInfo! #### Generic async functions + "Cancel async task by ID" asyncTaskCancel(id: String!): Boolean + "Get async task info by ID" asyncTaskInfo(id: String!, removeOnFinish: Boolean!): AsyncTaskInfo! } diff --git a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls index 5d21aaee78..4ec1051b9d 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls @@ -2,45 +2,80 @@ # Events API #################################################### -# Events sent by server +"Events sent by server" enum CBServerEventId { + "Configuration changed" cb_config_changed, + "Session log updated" cb_session_log_updated, + "Session websocket connected" cb_session_websocket_connected, + "Session state info updated" cb_session_state, + "Session expired" cb_session_expired, + "Datasource created" cb_datasource_created, + "Datasource updated" cb_datasource_updated, + "Datasource deleted" cb_datasource_deleted, + "Datasource folder created" cb_datasource_folder_created, + "Datasource folder updated" cb_datasource_folder_updated, + "Datasource folder deleted" cb_datasource_folder_deleted, + "Datasource disconnected" + cb_datasource_disconnected, + "Datasource connected" + cb_datasource_connected, + + "Resource manager resource created" cb_rm_resource_created, + "Resource manager resource updated" cb_rm_resource_updated, + "Resource manager resource deleted" cb_rm_resource_deleted, + "Resource manager project added" cb_rm_project_added, + "Resource manager project removed" cb_rm_project_removed, + "Object permissions updated (e.g., datasource)" cb_object_permissions_updated, + "Subject permissions updated (e.g., user, team)" cb_subject_permissions_updated, - cb_database_output_log_updated + "Database output log updated (e.g., DBMS in Oracle)" + cb_database_output_log_updated, + cb_ai_chat_message_chunk @since(version: "25.1.1") + cb_ai_chat_message_error @since(version: "25.1.1") + + "Transaction count updated" + cb_transaction_count @since(version: "24.3.3"), + + "Session async task info updated" + cb_session_task_info_updated @since(version: "24.3.1"), + "Workspace configuration updated" + cb_workspace_config_changed @since(version: "25.1.1") } -# Events sent by client +"Events sent by client" enum CBClientEventId { cb_client_topic_subscribe, cb_client_topic_unsubscribe, - cb_client_projects_active + cb_client_projects_active, + cb_client_session_ping } -# Client subscribes on topic to receive only related events +"Client subscribes on topic to receive only related events" enum CBEventTopic { cb_config, cb_session_log, @@ -51,22 +86,31 @@ enum CBEventTopic { cb_projects, cb_object_permissions, cb_subject_permissions, - cb_database_output_log + cb_database_output_log, + + cb_session_task, @since(version: "24.3.1") + + cb_datasource_connection, + cb_delete_temp_folder, + + cb_transaction @since(version: "24.3.3"), + cb_workspace_configuration @since(version: "25.1.1") + cb_ai @since(version: "25.1.1") } -# Base server event interface +"Base server event interface" interface CBServerEvent { id: CBServerEventId! topicId: CBEventTopic } -# Base client event interface +"Base client event interface" interface CBClientEvent { id: CBClientEventId! topicId: CBEventTopic } -# Datasource folder event +"Datasource folder event" type CBDatasourceFolderEvent implements CBServerEvent { id: CBServerEventId! topicId: CBEventTopic @@ -74,7 +118,7 @@ type CBDatasourceFolderEvent implements CBServerEvent { projectId: String! } -# Datasource event +"Datasource event" type CBDatasourceEvent implements CBServerEvent { id: CBServerEventId! topicId: CBEventTopic @@ -82,7 +126,7 @@ type CBDatasourceEvent implements CBServerEvent { projectId: String! } -# Resource manager event +"Resource manager event" type CBRMEvent implements CBServerEvent { id: CBServerEventId! topicId: CBEventTopic @@ -90,73 +134,79 @@ type CBRMEvent implements CBServerEvent { projectId: String! } -# Server Config event +"Server Config event" type CBConfigEvent implements CBServerEvent { id: CBServerEventId! topicId: CBEventTopic } -# Session log event +"Session log event" type CBSessionLogEvent implements CBServerEvent { id: CBServerEventId! topicId: CBEventTopic } -# WebSocket connected event +"WebSocket connected event" type WSSocketConnectedEvent implements CBServerEvent { id: CBServerEventId! topicId: CBEventTopic applicationRunId: String! } -# Session state info event +"Session state info event" type WSSessionStateEvent implements CBServerEvent { id: CBServerEventId! topicId: CBEventTopic + lastAccessTime: Int! remainingTime: Int! isValid: Boolean + isCacheExpired: Boolean + locale: String! + actionParameters: Object } -# Session expired event +"Session expired event" type WSSessionExpiredEvent implements CBServerEvent { id: CBServerEventId! topicId: CBEventTopic } -# RM project update event +"RM project update event" type CBProjectUpdateEvent implements CBServerEvent { id: CBServerEventId! topicId: CBEventTopic projectId: String! } -# Subject permission update event +"Subject permission update event" type CBSubjectPermissionUpdateEvent implements CBServerEvent { id: CBServerEventId! topicId: CBEventTopic } -# Topic subscription event +"Topic subscription event" type CBTopicEvent implements CBClientEvent { id: CBClientEventId! - topicId: CBEventTopic! # topic to subscribe / unsubscribe + "topic to subscribe / unsubscribe" + topicId: CBEventTopic! } -# Project event +"Project event" type CBProjectEvent implements CBClientEvent { id: CBClientEventId! topicId: CBEventTopic projectId: String! } -# Active projects event +"Active projects event" type CBProjectsActiveEvent implements CBClientEvent { id: CBClientEventId! topicId: CBEventTopic - projectIds: [String!]! # list of active projects + "list of active projects" + projectIds: [String!]! } -# Database output log event +"Database output log event" type CBDatabaseOutputLogEvent implements CBServerEvent { id: CBServerEventId! topicId: CBEventTopic @@ -165,11 +215,63 @@ type CBDatabaseOutputLogEvent implements CBServerEvent { eventTimestamp: Int! } -# Define the type for WSOutputLogInfo +"Define the type for WSOutputLogInfo" type WSOutputLogInfo { severity: String message: String - # Add more fields as needed + # Add more fields if needed +} + +"Async task info status event" +type WSAsyncTaskInfo @since(version: "24.3.1") { + id: CBServerEventId! + taskId: ID! + statusName: String + running: Boolean! +} + +"Datasource disconnect event" +type WSDataSourceDisconnectEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic + connectionId: String! + projectId: String! + timestamp: Int! +} +"Datasource connect event" +type WSDataSourceConnectEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic + connectionId: String! + projectId: String! + timestamp: Int! +} + +"Datasource count event in transactional mode" +type WSTransactionalCountEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic + contextId: String! + projectId: String! + connectionId: String! + transactionalCount: Int! +} + +type WSAIChatMessageChunkEvent implements CBServerEvent @since(version: "25.1.1") { + id: CBServerEventId! + topicId: CBEventTopic + conversationId: ID! + messageId: ID! + chunk: String + completed: Boolean! +} + +type WSAIChatMessageErrorEvent implements CBServerEvent @since(version: "25.1.1") { + id: CBServerEventId! + topicId: CBEventTopic + conversationId: ID! + messageId: ID! + errorMessage: String } extend type Query { diff --git a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls index c3e31fe09d..10dd6a8e69 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls @@ -24,17 +24,19 @@ type ObjectDetails { } type DatabaseObjectInfo { - # Object name + " Object name" name: String - # Description - optional + " Description - optional" description: String - # Object type. Java class name in most cases + " Object type. Java class name in most cases" type: String - # Read object properties. - # Optional parameter 'ids' filters properties by id. null means all properties. - # Note: property value reading may take a lot of time so don't read all property values always - # Examine property meta (features in particular) before reading them + """ + Read object properties. + Optional parameter 'ids' filters properties by id. null means all properties. + Note: property value reading may take a lot of time so don't read all property values always + Examine property meta (features in particular) before reading them + """ properties(filter: ObjectPropertyFilter): [ ObjectPropertyInfo ] # Optional properties @@ -45,43 +47,51 @@ type DatabaseObjectInfo { uniqueName: String state: String - # Features: script, scriptExtended, dataContainer, dataManipulator, - # entity, schema, catalog + "Features: script, scriptExtended, dataContainer, dataManipulator, entity, schema, catalog" features: [ String! ] - # Supported editors: ddl, permissions, sourceDeclaration, sourceDefinition + " Supported editors: ddl, permissions, sourceDeclaration, sourceDefinition" editors: [ String! ] } type NavigatorNodeInfo { - # Node ID - generally a full path to the node from root of tree + "Node ID - generally a full path to the node from root of tree" id: ID! - # Node human readable name + "Node URI - a unique path to a node including all parent nodes" + uri: ID! @since(version: "23.3.1") + "Node human readable name" name: String - #Node full name - fullName: String @deprecated # Use name parameter (23.2.0) - #Node plain name (23.2.0) + "Node full name" + fullName: String @deprecated(reason: "use name parameter (23.2.0)") + "Node plain name (23.2.0)" plainName: String - # Node icon path + "Node icon path" icon: String - # Node description + "Node description" description: String - # Node type + "Node type" nodeType: String - # Can this property have child nodes? + "Can this property have child nodes?" hasChildren: Boolean! - # Project id of the node + "Project id of the node" projectId: String - # Associated object. Maybe null for non-database objects + "Associated object. Maybe null for non-database objects" object: DatabaseObjectInfo - # Supported features: item, container, leaf - # canDelete, canRename + """ + Associated object. Return value depends on the node type - connectionId for connection node, resource path for resource node, etc. + null - if node currently not support this property + """ + objectId: String @since(version: "23.2.4") + + "Supported features: item, container, leaf, canDelete, canRename" features: [ String! ] - # Object detailed info. - # If is different than properties. It doesn't perform any expensive operation and doesn't require authentication. + """ + Object detailed info. + If is different than properties. It doesn't perform any expensive operation and doesn't require authentication. + """ nodeDetails: [ ObjectPropertyInfo! ] folder: Boolean! @@ -89,7 +99,7 @@ type NavigatorNodeInfo { navigable: Boolean! filtered: Boolean! - # Reads node filter. Expensive invocation, read only when it is really needed + "Reads node filter. Expensive invocation, read only when it is really needed" filter: NavigatorNodeFilter } @@ -117,19 +127,21 @@ type DatabaseStructContainers { extend type Query { - # Get child nodes + "Returns child nodes based on parent node path" navNodeChildren( parentPath: ID!, offset: Int, limit: Int, onlyFolders: Boolean): [ NavigatorNodeInfo! ]! - # Get node's parents + "Returns parent nodes for the specified node path" navNodeParents( nodePath: ID! ): [ NavigatorNodeInfo! ]! + "Returns node info for the specified node path" navNodeInfo( nodePath: ID! ): NavigatorNodeInfo! - navRefreshNode( nodePath: ID! ): Boolean + "Refreshes node based on the node path" + navRefreshNode( nodePath: ID! ): Boolean @deprecated # Use navReloadNode method from Mutation (24.2.4) # contextId currently not using navGetStructContainers( projectId: ID, connectionId: ID!, contextId: ID, catalog: ID ): DatabaseStructContainers! @@ -138,17 +150,22 @@ extend type Query { extend type Mutation { - # Rename node and returns new node name + "Reloads node and returns updated node info" + navReloadNode( nodePath: ID! ): NavigatorNodeInfo! + + "Renames node and returns new node name" navRenameNode( nodePath: ID!, newName: String! ): String - # Deletes nodes with specified IDs and returns number of deleted nodes + "Deletes nodes with specified IDs and returns number of deleted nodes" navDeleteNodes( nodePaths: [ID!]! ): Int - # Moves nodes with specified IDs to the connection folder + "Moves nodes with specified IDs to the connection folder" navMoveNodesToFolder( nodePaths: [ID!]!, folderPath: ID!): Boolean! - # Sets filter for the folder node. If both include and exclude are null then filter is removed. - # Node must be refreshed after applying filters. Node children can be changed + """ + Sets filter for the folder node. If both include and exclude are null then filter is removed. + Node must be refreshed after applying filters. Node children can be changed + """ navSetFolderFilter( nodePath: ID!, include: [String!], exclude: [String!]): Boolean! } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index d008e2b557..fe9ba82356 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -44,6 +44,7 @@ type SQLContextInfo { id: ID! projectId: ID! connectionId: ID! + autoCommit: Boolean defaultCatalog: String defaultSchema: String @@ -60,7 +61,7 @@ input SQLDataFilterConstraint { } input SQLDataFilter { - # Row offset. We use Float because offset may be bigger than 32 bit. + "Row offset. We use Float because offset may be bigger than 32 bit." offset: Float limit: Int @@ -79,18 +80,27 @@ type SQLResultColumn { dataKind: String typeName: String fullTypeName: String - # Column value max length. We use Float because it may be bigger than 32 bit. + "Column value max length. We use Float because it may be bigger than 32 bit" maxLength: Float scale: Int precision: Int required: Boolean! + autoGenerated: Boolean! readOnly: Boolean! readOnlyStatus: String - # Operations supported for this attribute + "Operations supported for this attribute" supportedOperations: [DataTypeLogicalOperation!]! + + "Description of the column" + description: String @since(version: "25.1.3") +} + +type SQLResultRowMetaData { + data: [Object]! + metaData: Object } type DatabaseDocument { @@ -101,17 +111,32 @@ type DatabaseDocument { } type SQLResultSet { + "Result set ID" id: ID! + "Returns list of columns in the result set" columns: [ SQLResultColumn ] - rows: [ [ Object ] ] + "Returns list of rows in the result set. Each row is an array of column values" + rows: [ [ Object ] ] @deprecated(reason: "use rowsWithMetaData (23.3.5)") + "Returns list of rows in the result set. Each row contains data and metadata" + rowsWithMetaData: [SQLResultRowMetaData] @since(version: "23.3.5") # True means that resultset was generated by single entity query # New rows can be added, old rows can be deleted singleEntity: Boolean! # server always returns hasMoreData = false hasMoreData: Boolean! - # can't update data or load LOB file if hasRowIdentifier = false + "Identifies if result set has row identifier. If true then it is possible update data or load LOB files" hasRowIdentifier: Boolean! + "Identifies if result has children collections. If true then children collections can be read from the result set" + hasChildrenCollection: Boolean! + "Identifies if result set supports dynamic trace. If true then dynamic trace can be read from the result set" + hasDynamicTrace: Boolean! @since(version: "24.1.2") + "Identifies if result set supports data filter. If true then data filter can be applied to the result set" + isSupportsDataFilter: Boolean! + "Identifies if result set is read-only. If true then no updates are allowed" + readOnly: Boolean! @since(version: "25.0.1") + "Status of read-only result set. If readOnly is true then this field contains reason why result set is read-only" + readOnlyStatus: String @since(version: "25.0.1") } type SQLQueryResults { @@ -144,6 +169,12 @@ type SQLExecuteInfo { input SQLResultRow { data: [ Object ]! updateValues: Object + metaData: Object +} + +input SQLResultRowMetaDataInput { + data: [Object] + metaData: Object! } type DataTypeLogicalOperation { @@ -196,17 +227,47 @@ type SQLScriptQuery { start: Int! end: Int! } + +#################################################### +# Dynamic trace info +#################################################### +type DynamicTraceProperty { + name: String! + value: String + description: String +} + +#################################################### +# Transactional info +#################################################### +type TransactionLogInfoItem { + id: Int! + time: DateTime! + type: String! + queryString: String! + durationMs: Int! + rows: Int! + result: String! +} +type TransactionLogInfos { + count: Int! + transactionLogInfos: [TransactionLogInfoItem!]! +} + + #################################################### # Query and Mutation #################################################### extend type Query { + "Returns SQL dialect info for the specified connection" sqlDialectInfo( projectId: ID, connectionId: ID! ): SQLDialectInfo - # Lists SQL contexts for a connection (optional) or returns the particular context info + "Lists SQL contexts for a connection (optional) or returns the particular context info" sqlListContexts( projectId: ID, connectionId: ID, contextId: ID ): [ SQLContextInfo ]! + "Returns proposals for SQL completion at the specified position in the query" sqlCompletionProposals( projectId: ID, connectionId: ID!, @@ -217,6 +278,7 @@ extend type Query { simpleMode: Boolean ): [ SQLCompletionProposal ] + "Formats SQL query and returns formatted query string" sqlFormatQuery( projectId: ID, connectionId: ID!, @@ -224,6 +286,7 @@ extend type Query { query: String! ): String! + "Returns suported operations for the specified attribute index in the results" sqlSupportedOperations( projectId: ID, connectionId: ID!, @@ -232,30 +295,34 @@ extend type Query { attributeIndex: Int! ): [DataTypeLogicalOperation!]! - # List of all available entity query generators - sqlEntityQueryGenerators(nodePathList: [String!]! - ): [SQLQueryGenerator!]! - - # Options: - # fullyQualifiedNames: Boolean - # compactSQL: Boolean - # showComments: Boolean - # showPermissions: Boolean - # showFullDdl: Boolean - # excludeAutoGeneratedColumn: Boolean - # useCustomDataFormat: Boolean + "Returns list of all available entity query generators" + sqlEntityQueryGenerators(nodePathList: [String!]!): [SQLQueryGenerator!]! + + """ + Generates SQL query for the specified entity query generator. + Available options: + fullyQualifiedNames - Use fully qualified names for entities + compactSQL - Use compact SQL format + showComments - Show comments in generated SQL + showPermissions - Show permissions in generated SQL + showFullDdl - Show full DDL in generated SQL + excludeAutoGeneratedColumn - Exclude auto-generated columns in generated SQL + useCustomDataFormat - Use custom data format for generated SQL + """ sqlGenerateEntityQuery( generatorId: String!, options: Object!, nodePathList: [String!]! ): String! + "Parses SQL script and returns script info with queries start and end positions" sqlParseScript( projectId: ID, connectionId: ID!, script: String! ): SQLScriptInfo! + "Parses SQL query and returns query info with start and end positions" sqlParseQuery( projectId: ID, connectionId: ID!, @@ -263,6 +330,7 @@ extend type Query { position: Int! ): SQLScriptQuery! + "Generates SQL query for grouping data in the specified results" sqlGenerateGroupingQuery( projectId: ID, contextId: ID!, @@ -275,13 +343,16 @@ extend type Query { } extend type Mutation { + "Creates SQL context for the specified connection" sqlContextCreate( projectId: ID, connectionId: ID!, defaultCatalog: String, defaultSchema: String ): SQLContextInfo! + "Sets SQL context defaults" sqlContextSetDefaults( projectId: ID, connectionId: ID!, contextId: ID!, defaultCatalog: ID, defaultSchema: ID ): Boolean! + "Destroys SQL context and closes all results" sqlContextDestroy( projectId: ID, connectionId: ID!, contextId: ID! ): Boolean! - # Execute SQL and return results + "Creates async task for executing SQL query" asyncSqlExecuteQuery( projectId: ID, connectionId: ID!, @@ -293,7 +364,7 @@ extend type Mutation { readLogs: Boolean # added 23.2.1 ): AsyncTaskInfo! - # Read data from table + "Creates async task for reading data from the container node" asyncReadDataFromContainer( projectId: ID, connectionId: ID!, @@ -304,10 +375,29 @@ extend type Mutation { dataFormat: ResultDataFormat ): AsyncTaskInfo! - # Close results (free resources) + "Returns transaction log info for the specified project, connection and context" + getTransactionLogInfo( + projectId: ID!, + connectionId: ID!, + contextId: ID! + ): TransactionLogInfos! + + "Closes SQL results (free resources)" sqlResultClose(projectId: ID, connectionId: ID!, contextId: ID!, resultId: ID!): Boolean! - # Update multiple cell values + "Creates async task for updating results data in batch mode" + asyncUpdateResultsDataBatch( + projectId: ID!, + connectionId: ID!, + contextId: ID!, + resultsId: ID!, + + updatedRows: [SQLResultRow!], + deletedRows: [SQLResultRow!], + addedRows: [SQLResultRow!], + ): AsyncTaskInfo! @since(version: "25.0.0") + + "Synchronously updates results data in batch mode" updateResultsDataBatch( projectId: ID, connectionId: ID!, @@ -317,9 +407,9 @@ extend type Mutation { updatedRows: [ SQLResultRow! ], deletedRows: [ SQLResultRow! ], addedRows: [ SQLResultRow! ], - ): SQLExecuteInfo! + ): SQLExecuteInfo! @deprecated(reason: "use async function (25.0.0)") - # Return SQL script for cell values update + "Returns SQL script for cell values update" updateResultsDataBatchScript( projectId: ID, connectionId: ID!, @@ -331,7 +421,7 @@ extend type Mutation { addedRows: [ SQLResultRow! ], ): String! - #Return BLOB name + "Returns BLOB value" readLobValue( projectId: ID, connectionId: ID!, @@ -339,12 +429,32 @@ extend type Mutation { resultsId: ID!, lobColumnIndex: Int!, row: [ SQLResultRow! ]! - ): String! + ): String! @deprecated(reason: "use sqlReadLobValue (23.3.3)") - # Returns SQLExecuteInfo + "Returns BLOB value as Base64 encoded string" + sqlReadLobValue( + projectId: ID, + connectionId: ID!, + contextId: ID!, + resultsId: ID!, + lobColumnIndex: Int!, + row: SQLResultRow! + ): String! @since(version: "23.3.3") + + "Returns full string value ignoring any limits" + sqlReadStringValue( + projectId: ID, + connectionId: ID!, + contextId: ID!, + resultsId: ID!, + columnIndex: Int!, + row: SQLResultRow! + ): String! @since(version: "23.3.3") + + "Returns SQL execution results for async SQL execute task" asyncSqlExecuteResults(taskId: ID!): SQLExecuteInfo ! - # Read data from table + "Creates async task to generating SQL execution plan" asyncSqlExplainExecutionPlan( projectId: ID, connectionId: ID!, @@ -353,7 +463,48 @@ extend type Mutation { configuration: Object! ): AsyncTaskInfo! - # Returns SQLExecutionPlan + "Returns SQL execution plan for async SQL explain task" asyncSqlExplainExecutionPlanResult(taskId: ID!): SQLExecutionPlan ! + "Creates async task to count rows in SQL results" + asyncSqlRowDataCount( + projectId: ID, + connectionId: ID!, + contextId: ID!, + resultsId: ID! + ): AsyncTaskInfo! + + "Returns row count for async SQL row data count task" + asyncSqlRowDataCountResult(taskId: ID!): Int! + + "Reads dynamic trace from provided database results" + sqlGetDynamicTrace( + projectId: ID, + connectionId: ID!, + contextId: ID!, + resultsId: ID! + ): [DynamicTraceProperty!]! @since(version: "24.1.2") + + "Creates async task to set auto-commit mode" + asyncSqlSetAutoCommit( + projectId: ID!, + connectionId: ID!, + contextId: ID!, + autoCommit: Boolean! + ): AsyncTaskInfo! @since(version: "24.0.1") + + "Creates async task to commit transaction" + asyncSqlCommitTransaction( + projectId: ID!, + connectionId: ID!, + contextId: ID! + ): AsyncTaskInfo! @since(version: "24.0.1") + + "Creates async task to rollback transaction" + asyncSqlRollbackTransaction( + projectId: ID!, + connectionId: ID!, + contextId: ID! + ): AsyncTaskInfo! @since(version: "24.0.1") + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionAccessDenied.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionAccessDenied.java index 8303aab196..2fd3cdf81d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionAccessDenied.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionAccessDenied.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionLicenseRequired.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionLicenseRequired.java index eb8fce6acc..2e798e7381 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionLicenseRequired.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionLicenseRequired.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionServerNotInitialized.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionServerNotInitialized.java new file mode 100644 index 0000000000..610d7c414e --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebExceptionServerNotInitialized.java @@ -0,0 +1,27 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +/** + * "Access denied" exception + */ +public class DBWebExceptionServerNotInitialized extends DBWebException { + + public DBWebExceptionServerNotInitialized(String message) { + super(message, ERROR_CODE_SERVER_NOT_INITIALIZED); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebAction.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebAction.java index 70ce6cec1c..24bcaa85b6 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebAction.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebAction.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -33,4 +33,8 @@ boolean authRequired() default true; + boolean initializationRequired() default true; + + String[] requireGlobalPermissions() default {}; + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebActionSet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebActionSet.java index d417c83412..94e0e8526d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebActionSet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebActionSet.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebProjectAction.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebProjectAction.java index f09d83919c..f9f724e4bc 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebProjectAction.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebProjectAction.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java index 5dc21c6344..f27af0efc9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -18,21 +18,28 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.InstanceCreator; import io.cloudbeaver.model.WebConnectionConfig; import io.cloudbeaver.model.WebNetworkHandlerConfigInput; +import io.cloudbeaver.model.WebPropertyInfo; +import io.cloudbeaver.model.app.ServletApplication; +import io.cloudbeaver.model.app.WebAppConfiguration; +import io.cloudbeaver.model.rm.DBNResourceManagerResource; import io.cloudbeaver.model.session.WebActionParameters; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.model.utils.ConfigurationUtils; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; -import io.cloudbeaver.server.CBAppConfig; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.server.WebAppUtils; +import io.cloudbeaver.server.WebApplication; +import io.cloudbeaver.service.navigator.WebPropertyFilter; +import io.cloudbeaver.utils.ServletAppUtils; import io.cloudbeaver.utils.WebCommonUtils; import io.cloudbeaver.utils.WebDataSourceUtils; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBConstants; import org.jkiss.dbeaver.model.DBPDataSourceContainer; import org.jkiss.dbeaver.model.access.DBAAuthCredentials; import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; @@ -40,10 +47,9 @@ import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNativeCredentials; -import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; -import org.jkiss.dbeaver.model.navigator.DBNModel; -import org.jkiss.dbeaver.model.navigator.DBNProject; +import org.jkiss.dbeaver.model.navigator.*; import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration; +import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; import org.jkiss.dbeaver.model.rm.RMProjectType; import org.jkiss.dbeaver.registry.DataSourceDescriptor; import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings; @@ -52,10 +58,12 @@ import org.jkiss.dbeaver.registry.driver.DriverDescriptor; import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor; import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry; +import org.jkiss.dbeaver.runtime.properties.PropertyCollector; import org.jkiss.utils.CommonUtils; import java.io.InputStream; import java.util.*; +import java.util.stream.Collectors; /** * Various constants @@ -90,10 +98,6 @@ public static DBPDataSourceRegistry getGlobalDataSourceRegistry() throws DBWebEx return WebDataSourceUtils.getGlobalDataSourceRegistry(); } - public static DBPDataSourceRegistry getGlobalRegistry(WebSession session) { - return session.getProjectById(WebAppUtils.getGlobalProjectId()).getDataSourceRegistry(); - } - public static InputStream openStaticResource(String path) { return WebServiceUtils.class.getClassLoader().getResourceAsStream(path); } @@ -101,13 +105,7 @@ public static InputStream openStaticResource(String path) { @NotNull public static DBPDataSourceContainer createConnectionFromConfig(WebConnectionConfig config, DBPDataSourceRegistry registry) throws DBWebException { DBPDataSourceContainer newDataSource; - if (!CommonUtils.isEmpty(config.getTemplateId())) { - DBPDataSourceContainer tpl = registry.getDataSource(config.getTemplateId()); - if (tpl == null) { - throw new DBWebException("Template connection '" + config.getTemplateId() + "' not found"); - } - newDataSource = registry.createDataSource(tpl); - } else if (!CommonUtils.isEmpty(config.getDriverId())) { + if (!CommonUtils.isEmpty(config.getDriverId())) { String driverId = config.getDriverId(); if (CommonUtils.isEmpty(driverId)) { throw new DBWebException("Driver not specified"); @@ -126,16 +124,16 @@ public static DBPDataSourceContainer createConnectionFromConfig(WebConnectionCon newDataSource.setSavePassword(true); newDataSource.setName(config.getName()); newDataSource.setDescription(config.getDescription()); + newDataSource.setConnectionReadOnly(config.isReadOnly()); if (config.getFolder() != null) { newDataSource.setFolder(registry.getFolder(config.getFolder())); } - ((DataSourceDescriptor)newDataSource).setTemplate(config.isTemplate()); - // Set default navigator settings - DataSourceNavigatorSettings navSettings = new DataSourceNavigatorSettings( - CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings()); - //navSettings.setShowSystemObjects(false); - ((DataSourceDescriptor)newDataSource).setNavigatorSettings(navSettings); + ServletApplication app = ServletAppUtils.getServletApplication(); + if (app instanceof WebApplication webApplication) { + ((DataSourceDescriptor) newDataSource).setNavigatorSettings( + webApplication.getAppConfiguration().getDefaultNavigatorSettings()); + } saveAuthProperties( newDataSource, @@ -150,22 +148,7 @@ public static DBPDataSourceContainer createConnectionFromConfig(WebConnectionCon } public static void setConnectionConfiguration(DBPDriver driver, DBPConnectionConfiguration dsConfig, WebConnectionConfig config) { - if (!CommonUtils.isEmpty(config.getUrl())) { - dsConfig.setUrl(config.getUrl()); - } else { - if (config.getHost() != null) { - dsConfig.setHostName(config.getHost()); - } - if (config.getPort() != null) { - dsConfig.setHostPort(config.getPort()); - } - if (config.getDatabaseName() != null) { - dsConfig.setDatabaseName(config.getDatabaseName()); - } - if (config.getServerName() != null) { - dsConfig.setServerName(config.getServerName()); - } - } + setMainProperties(dsConfig, config); if (config.getProperties() != null) { Map newProps = new LinkedHashMap<>(); for (Map.Entry pe : config.getProperties().entrySet()) { @@ -182,6 +165,14 @@ public static void setConnectionConfiguration(DBPDriver driver, DBPConnectionCon if (config.getAuthModelId() != null) { dsConfig.setAuthModelId(config.getAuthModelId()); } + if (config.getKeepAliveInterval() >= 0) { + dsConfig.setKeepAliveInterval(config.getKeepAliveInterval()); + } + if (config.isDefaultAutoCommit() != null) { + dsConfig.getBootstrap().setDefaultAutoCommit(config.isDefaultAutoCommit()); + } + dsConfig.getBootstrap().setDefaultCatalogName(config.getDefaultCatalogName()); + dsConfig.getBootstrap().setDefaultSchemaName(config.getDefaultSchemaName()); // Save provider props if (config.getProviderProperties() != null) { dsConfig.setProviderProperties(new LinkedHashMap<>()); @@ -216,6 +207,40 @@ public static void setConnectionConfiguration(DBPDriver driver, DBPConnectionCon } } + private static void setMainProperties(DBPConnectionConfiguration dsConfig, WebConnectionConfig config) { + if (CommonUtils.isNotEmpty(config.getUrl())) { + dsConfig.setUrl(config.getUrl()); + return; + } + if (config.getMainPropertyValues() != null) { + for (Map.Entry e : config.getMainPropertyValues().entrySet()) { + if (e.getValue() == null) { + continue; + } + switch (e.getKey()) { + case DBConstants.PROP_HOST -> dsConfig.setHostName(CommonUtils.toString(e.getValue())); + case DBConstants.PROP_PORT -> dsConfig.setHostPort(CommonUtils.toString(e.getValue())); + case DBConstants.PROP_DATABASE -> dsConfig.setDatabaseName(CommonUtils.toString(e.getValue())); + case DBConstants.PROP_SERVER -> dsConfig.setServerName(CommonUtils.toString(e.getValue())); + default -> throw new IllegalStateException("Unexpected value: " + e.getKey()); + } + } + return; + } + if (config.getHost() != null) { + dsConfig.setHostName(config.getHost()); + } + if (config.getPort() != null) { + dsConfig.setHostPort(config.getPort()); + } + if (config.getDatabaseName() != null) { + dsConfig.setDatabaseName(config.getDatabaseName()); + } + if (config.getServerName() != null) { + dsConfig.setServerName(config.getServerName()); + } + } + public static void saveAuthProperties( @NotNull DBPDataSourceContainer dataSourceContainer, @NotNull DBPConnectionConfiguration configuration, @@ -262,15 +287,7 @@ public static void saveAuthProperties( configuration.setAuthProperties(currentAuthProps); } if (!authProperties.isEmpty()) { - - // Make new Gson parser with type adapters to deserialize into existing credentials - InstanceCreator credTypeAdapter = type -> credentials; - Gson credGson = new GsonBuilder() - .setLenient() - .registerTypeAdapter(credentials.getClass(), credTypeAdapter) - .create(); - - credGson.fromJson(credGson.toJsonTree(authProperties), credentials.getClass()); + WebDataSourceUtils.updateCredentialsFromProperties(credentials, authProperties); } configuration.getAuthModel().saveCredentials(dataSourceContainer, configuration, credentials); @@ -282,12 +299,6 @@ public static DBNBrowseSettings parseNavigatorSettings(Map setti gson.toJsonTree(settingsMap), DataSourceNavigatorSettings.class); } - public static void checkServerConfigured() throws DBWebException { - if (CBApplication.getInstance().isConfigurationMode()) { - throw new DBWebException("Server is in configuration mode"); - } - } - public static void fireActionParametersOpenEditor(WebSession webSession, DBPDataSourceContainer dataSource, boolean addEditorName) { Map actionParameters = new HashMap<>(); actionParameters.put("action", "open-sql-editor"); @@ -306,19 +317,28 @@ public static String getConnectionContainerInfo(DBPDataSourceContainer container return container.getName() + " [" + container.getId() + "]"; } - public static void updateConfigAndRefreshDatabases(WebSession session, String projectId) { - DBNProject projectNode = session.getNavigatorModel().getRoot().getProjectNode(session.getProjectById(projectId)); - DBNModel.updateConfigAndRefreshDatabases(projectNode.getDatabases()); + public static void refreshDatabases(WebSession session, String projectId) throws DBWebException { + DBNProject projectNode = session.getNavigatorModelOrThrow().getRoot().getProjectNode(session.getProjectById(projectId)); + if (projectNode != null) { + projectNode.getDatabases().refreshChildren(); + } } public static boolean isGlobalProject(DBPProject project) { - return project.getId().equals(RMProjectType.GLOBAL.getPrefix() + "_" + CBApplication.getInstance().getDefaultProjectName()); + return project.getId() + .equals(RMProjectType.GLOBAL.getPrefix() + "_" + ServletAppUtils.getServletApplication() + .getDefaultProjectName()); } public static List getEnabledAuthProviders() { List result = new ArrayList<>(); - CBAppConfig appConfig = CBApplication.getInstance().getAppConfiguration(); - String[] authProviders = appConfig.getEnabledAuthProviders(); + String[] authProviders = null; + try { + authProviders = ServletAppUtils.getAuthApplication().getAuthConfiguration().getEnabledAuthProviders(); + } catch (DBException e) { + log.error(e.getMessage(), e); + return List.of(); + } for (String apId : authProviders) { WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(apId); if (authProvider != null) { @@ -328,4 +348,76 @@ public static List getEnabledAuthProviders() { return result; } + /** + * Returns set of applicable ids of drivers. + */ + @NotNull + public static Set getApplicableDriversIds() { + return WebAppUtils.getWebApplication().getDriverRegistry().getApplicableDrivers().stream() + .map(DBPDriver::getId) + .collect(Collectors.toSet()); + } + + /** + * Returns filtered properties collected from object. + */ + @NotNull + public static WebPropertyInfo[] getObjectFilteredProperties( + @NotNull WebSession session, + @NotNull Object object, + @Nullable WebPropertyFilter filter + ) { + PropertyCollector propertyCollector = new PropertyCollector(object, true); + propertyCollector.setLocale(session.getLocale()); + propertyCollector.collectProperties(); + List webProps = new ArrayList<>(); + for (DBPPropertyDescriptor prop : propertyCollector.getProperties()) { + if (filter != null && !CommonUtils.isEmpty(filter.getIds()) && !filter.getIds().contains(CommonUtils.toString(prop.getId()))) { + continue; + } + WebPropertyInfo webProperty = new WebPropertyInfo(session, prop, propertyCollector); + if (filter != null) { + if (!CommonUtils.isEmpty(filter.getFeatures()) && !webProperty.hasAnyFeature(filter.getFeatures())) { + continue; + } + if (!CommonUtils.isEmpty(filter.getCategories()) && !filter.getCategories().contains(webProperty.getCategory())) { + continue; + } + } + boolean propertyAcceptable = true; + if (prop.getRequiredFeatures() != null) { + for (String feature : prop.getRequiredFeatures()) { + propertyAcceptable = session.getApplication().getAppConfiguration().isFeatureEnabled(feature); + if (!propertyAcceptable) { + break; + } + } + } + if (propertyAcceptable) { + webProps.add(webProperty); + } + } + return webProps.toArray(new WebPropertyInfo[0]); + } + + /** + * Check whether driver is enabled + * + * @param driver driver + * @return true if driver is enabled + */ + public static boolean isDriverEnabled(@NotNull DBPDriver driver) { + WebAppConfiguration config = WebAppUtils.getWebApplication().getAppConfiguration(); + return ConfigurationUtils.isDriverEnabled( + driver, + config.getEnabledDrivers(), + config.getDisabledDrivers() + ); + } + + public static boolean isFolder(@NotNull DBNNode node) { + return (node instanceof DBNContainer && !(node instanceof DBNDataSource)) + || (node instanceof DBNResourceManagerResource + && ((DBNResourceManagerResource) node).getResource().isFolder()); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebCommandContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebCommandContext.java index bf4c3f6833..f34d2ce3c2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebCommandContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebCommandContext.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionConfig.java deleted file mode 100644 index ba7ee96f65..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionConfig.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * 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.model; - -import org.jkiss.dbeaver.model.connection.DBPDriverConfigurationType; -import org.jkiss.dbeaver.model.data.json.JSONUtils; -import org.jkiss.dbeaver.model.meta.Property; -import org.jkiss.utils.CommonUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Web connection config - */ -public class WebConnectionConfig { - - private String connectionId; - private String templateId; - private String driverId; - - private boolean template; - private boolean readOnly; - - private String host; - private String port; - private String serverName; - private String databaseName; - private String url; - - private String name; - private String description; - private String folder; - private Map properties; - private String userName; - private String userPassword; - - private String authModelId; - private Map credentials; - private boolean saveCredentials; - private boolean sharedCredentials; - private Map providerProperties; - private List networkHandlersConfig; - private DBPDriverConfigurationType configurationType; - - public WebConnectionConfig() { - } - - public WebConnectionConfig(Map params) { - if (!CommonUtils.isEmpty(params)) { - connectionId = JSONUtils.getString(params, "connectionId"); - templateId = JSONUtils.getString(params, "templateId"); - String dataSourceId = JSONUtils.getString(params, "dataSourceId"); - if (CommonUtils.isEmpty(templateId) && !CommonUtils.isEmpty(dataSourceId)) { - templateId = dataSourceId; - } - driverId = JSONUtils.getString(params, "driverId"); - - template = JSONUtils.getBoolean(params, "template"); - readOnly = JSONUtils.getBoolean(params, "readOnly"); - - host = JSONUtils.getString(params, "host"); - port = JSONUtils.getString(params, "port"); - serverName = JSONUtils.getString(params, "serverName"); - databaseName = JSONUtils.getString(params, "databaseName"); - url = JSONUtils.getString(params, "url"); - - name = JSONUtils.getString(params, "name"); - description = JSONUtils.getString(params, "description"); - folder = JSONUtils.getString(params, "folder"); - - properties = JSONUtils.getObjectOrNull(params, "properties"); - userName = JSONUtils.getString(params, "userName"); - userPassword = JSONUtils.getString(params, "userPassword"); - - authModelId = JSONUtils.getString(params, "authModelId"); - credentials = JSONUtils.getObjectOrNull(params, "credentials"); - saveCredentials = JSONUtils.getBoolean(params, "saveCredentials"); - sharedCredentials = JSONUtils.getBoolean(params, "sharedCredentials"); - - providerProperties = JSONUtils.getObjectOrNull(params, "providerProperties"); - - String configType = JSONUtils.getString(params, "configurationType"); - configurationType = configType == null ? null : DBPDriverConfigurationType.valueOf(configType); - - networkHandlersConfig = new ArrayList<>(); - for (Map nhc : JSONUtils.getObjectList(params, "networkHandlersConfig")) { - networkHandlersConfig.add(new WebNetworkHandlerConfigInput(nhc)); - } - } - } - - @Property - public String getConnectionId() { - return connectionId; - } - - @Property - public String getTemplateId() { - return templateId; - } - - @Property - public String getDriverId() { - return driverId; - } - - @Property - public boolean isTemplate() { - return template; - } - - @Property - public boolean isReadOnly() { - return readOnly; - } - - @Property - public String getName() { - return name; - } - - @Property - public String getDescription() { - return description; - } - - @Property - public String getFolder() { - return folder; - } - - @Property - public String getHost() { - return host; - } - - @Property - public String getPort() { - return port; - } - - @Property - public String getServerName() { - return serverName; - } - - @Property - public String getDatabaseName() { - return databaseName; - } - - @Property - public String getUrl() { - return url; - } - - @Property - public Map getProperties() { - return properties; - } - - @Property - public String getUserName() { - return userName; - } - - @Property - public String getUserPassword() { - return userPassword; - } - - @Property - public String getAuthModelId() { - return authModelId; - } - - @Property - public DBPDriverConfigurationType getConfigurationType() { - return configurationType; - } - - @Property - public Map getCredentials() { - return credentials; - } - - public List getNetworkHandlersConfig() { - return networkHandlersConfig; - } - - @Property - public boolean isSaveCredentials() { - return saveCredentials; - } - - @Property - public boolean isSharedCredentials() { - return sharedCredentials; - } - - public void setSaveCredentials(boolean saveCredentials) { - this.saveCredentials = saveCredentials; - } - - @Property - public Map getProviderProperties() { - return providerProperties; - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDataSourceConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDataSourceConfig.java index 9fb8fc61f0..b9ac67edf4 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDataSourceConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDataSourceConfig.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseAuthModel.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseAuthModel.java index cd5009bbe3..65333f22fd 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseAuthModel.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseAuthModel.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java index 17c9414e0e..096afc7d6c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -19,7 +19,6 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.ConfigurationUtils; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBConstants; @@ -31,9 +30,11 @@ import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor; import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry; import org.jkiss.dbeaver.runtime.properties.PropertySourceCustom; +import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.Map; /** @@ -46,7 +47,7 @@ public class WebDatabaseDriverInfo { public static final String URL_DATABASE_FIELD = ".*(?:\\{(?:database|file|folder)}).*"; private final WebSession webSession; private final DBPDriver driver; - private String id; + private final String id; public WebDatabaseDriverInfo(WebSession webSession, DBPDriver driver) { this.webSession = webSession; @@ -79,6 +80,11 @@ public String getIconBig() { return WebServiceUtils.makeIconId(driver.getIconBig()); } + @Property + public String getDriverId() { + return driver.getId(); + } + @Property public String getProviderId() { return driver.getProviderId(); @@ -174,9 +180,10 @@ public Map getDefaultConnectionProperties() { public WebPropertyInfo[] getDriverProperties() throws DBWebException { try { DBPConnectionConfiguration cfg = new DBPConnectionConfiguration(); - cfg.setUrl(driver.getSampleURL()); + cfg.setUrl(CommonUtils.notEmpty(driver.getSampleURL())); cfg.setHostName(DBConstants.HOST_LOCALHOST); cfg.setHostPort(driver.getDefaultPort()); + cfg.setDatabaseName(driver.getDefaultDatabase()); cfg.setUrl(driver.getConnectionURL(cfg)); DBPPropertyDescriptor[] properties = driver.getDataSourceProvider().getConnectionProperties(webSession.getProgressMonitor(), driver, cfg); if (properties == null) { @@ -190,7 +197,7 @@ public WebPropertyInfo[] getDriverProperties() throws DBWebException { return Arrays.stream(properties) .map(p -> new WebPropertyInfo(webSession, p, propertySource)).toArray(WebPropertyInfo[]::new); } catch (DBException e) { - log.error("Error reading driver properties", e); + log.error("Error reading driver properties:\n" + e.getMessage()); return new WebPropertyInfo[0]; } } @@ -209,12 +216,12 @@ public String[] getApplicableAuthModels() { @Property public String[] getApplicableNetworkHandlers() { - if (driver.isEmbedded()) { - return new String[0]; + if (!driver.isEmbedded() || CommonUtils.toBoolean(driver.getDriverParameter(DBConstants.DRIVER_PARAM_ENABLE_NETWORK_PARAMETERS))) { + return NetworkHandlerRegistry.getInstance().getDescriptors(driver).stream() + .filter(h -> !h.isDesktopHandler()) + .map(NetworkHandlerDescriptor::getId).toArray(String[]::new); } - return NetworkHandlerRegistry.getInstance().getDescriptors(driver).stream() - .filter(h -> !h.isDesktopHandler()) - .map(NetworkHandlerDescriptor::getId).toArray(String[]::new); + return new String[0]; } @Property @@ -227,6 +234,22 @@ public String getDefaultAuthModel() { return AuthModelDatabaseNative.ID; } + @Property + public WebPropertyInfo[] getMainProperties() { + DBPPropertyDescriptor[] properties = driver.getMainPropertyDescriptors(); + // set default values to main properties + Map defaultValues = new LinkedHashMap<>(); + defaultValues.put(DBConstants.PROP_HOST, getDefaultHost()); + defaultValues.put(DBConstants.PROP_PORT, getDefaultPort()); + defaultValues.put(DBConstants.PROP_DATABASE, getDefaultDatabase()); + defaultValues.put(DBConstants.PROP_SERVER, getDefaultServer()); + PropertySourceCustom propertySource = new PropertySourceCustom(properties, defaultValues); + + return Arrays.stream(properties) + .map(p -> new WebPropertyInfo(webSession, p, propertySource)) + .toArray(WebPropertyInfo[]::new); + } + @Property public WebPropertyInfo[] getProviderProperties() { return Arrays.stream(driver.getProviderPropertyDescriptors()) @@ -234,9 +257,13 @@ public WebPropertyInfo[] getProviderProperties() { .toArray(WebPropertyInfo[]::new); } + public WebPropertyInfo[] getExpertSettingsProperties() { + return WebServiceUtils.getObjectFilteredProperties(webSession, new WebExpertSettingsProperties(driver), null); + } + @Property public boolean isEnabled() { - return ConfigurationUtils.isDriverEnabled(driver); + return WebServiceUtils.isDriverEnabled(driver); } @Property @@ -262,7 +289,28 @@ public boolean getRequiresDatabaseName() { @Property public WebDriverLibraryInfo[] getDriverLibraries() { return driver.getDriverLibraries().stream() - .map(dbpDriverLibrary -> new WebDriverLibraryInfo(webSession, dbpDriverLibrary)) + .filter(library -> !library.isDisabled()) + .map(library -> new WebDriverLibraryInfo(driver, library)) .toArray(WebDriverLibraryInfo[]::new); } + + @Property + public boolean isDriverInstalled() { + return driver.getDefaultDriverLoader().isDriverInstalled(); + } + + @Property + public boolean isDownloadable() { + return driver.getDriverLibraries().stream().anyMatch(DBPDriverLibrary::isDownloadable); + } + + @Property + public boolean getUseCustomPage() { + return !ArrayUtils.isEmpty(driver.getMainPropertyDescriptors()); + } + + @Property + public boolean isSafeEmbeddedDriver() { + return CommonUtils.toBoolean(driver.getDriverParameter(DBConstants.PARAM_SAFE_EMBEDDED_DRIVER)); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatasourceAccessCheckHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatasourceAccessCheckHandler.java deleted file mode 100644 index 50cb70beb5..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatasourceAccessCheckHandler.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.model; - -import io.cloudbeaver.server.ConfigurationUtils; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.DBPDataSourceContainer; -import org.jkiss.dbeaver.model.DBPDataSourceHandler; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; - -public class WebDatasourceAccessCheckHandler implements DBPDataSourceHandler { - @Override - public void beforeConnect(DBRProgressMonitor monitor, @NotNull DBPDataSourceContainer dataSourceContainer) throws DBException { - if (!ConfigurationUtils.isDriverEnabled(dataSourceContainer.getDriver())) { - throw new DBException("Driver disabled"); - } - } - - @Override - public void beforeDisconnect(DBRProgressMonitor monitor, @NotNull DBPDataSourceContainer dataSourceContainer) throws DBException { - - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryFileInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryFileInfo.java new file mode 100644 index 0000000000..605b98c5d1 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryFileInfo.java @@ -0,0 +1,53 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model; + +import io.cloudbeaver.WebServiceUtils; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.DBIcon; +import org.jkiss.dbeaver.model.connection.DBPDriverLibrary; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.registry.driver.DriverFileInfo; + +public class WebDriverLibraryFileInfo { + + @NotNull + private final DriverFileInfo fileInfo; + + public WebDriverLibraryFileInfo(@NotNull DriverFileInfo fileInfo) { + this.fileInfo = fileInfo; + } + + + @Property + public String getId() { + return fileInfo.getId(); + } + + @Property + public String getFileName() { + return fileInfo.toString(); + } + + @Property + public String getIcon() { + if (fileInfo.getType() == DBPDriverLibrary.FileType.license) { + return WebServiceUtils.makeIconId(DBIcon.TYPE_TEXT); + } + return WebServiceUtils.makeIconId(DBIcon.JAR); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java index 3ce4be899a..629a8cfe47 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,18 +17,24 @@ package io.cloudbeaver.model; import io.cloudbeaver.WebServiceUtils; -import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.connection.DBPDriverLibrary; import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.registry.driver.DriverDescriptor; + +import java.util.List; public class WebDriverLibraryInfo { - private final WebSession webSession; + @NotNull + private final DBPDriver driver; + @NotNull private final DBPDriverLibrary driverLibrary; - public WebDriverLibraryInfo(@NotNull WebSession webSession, @NotNull DBPDriverLibrary driverLibrary) { - this.webSession = webSession; + public WebDriverLibraryInfo(@NotNull DBPDriver driver, @NotNull DBPDriverLibrary driverLibrary) { + this.driver = driver; this.driverLibrary = driverLibrary; } @@ -43,6 +49,18 @@ public String getName() { return driverLibrary.getDisplayName(); } + @Property + @Nullable + public List getLibraryFiles() { + var libraryFiles = ((DriverDescriptor) driver).getDefaultDriverLoader().getLibraryFiles(driverLibrary); + if (libraryFiles == null) { + return null; + } + return libraryFiles.stream() + .map(WebDriverLibraryFileInfo::new) + .toList(); + } + @Property public String getIcon() { return WebServiceUtils.makeIconId(driverLibrary.getIcon()); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebGroupPropertiesInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebGroupPropertiesInfo.java new file mode 100644 index 0000000000..302533c818 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebGroupPropertiesInfo.java @@ -0,0 +1,59 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model; + +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.impl.PropertyDescriptor; +import org.jkiss.dbeaver.model.impl.PropertyGroupDescriptor; +import org.jkiss.dbeaver.model.meta.Property; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class WebGroupPropertiesInfo { + @NotNull + private final WebSession webSession; + @NotNull + private final Collection> groups; + + public WebGroupPropertiesInfo( + @NotNull WebSession webSession, + @NotNull Collection> groups + ) { + this.webSession = webSession; + this.groups = groups; + } + + @NotNull + @Property + public List> getGroups() { + return groups.stream() + .map(WebSettingsGroupInfo::new) + .collect(Collectors.toList()); + } + + @NotNull + @Property + public List getSettings() { + return groups.stream() + .flatMap(group -> group.getSettings().stream()) + .map(setting -> new WebPropertyInfo(webSession, setting)) + .collect(Collectors.toList()); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkEndpointInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkEndpointInfo.java index 4bf8de2cf7..c3484d2442 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkEndpointInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkEndpointInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkHandlerDescriptor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkHandlerDescriptor.java index 5fb0837427..40d99f50c9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkHandlerDescriptor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkHandlerDescriptor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java index 4d09223c83..3e5940060e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,7 +16,7 @@ */ package io.cloudbeaver.model; -import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.utils.ServletAppUtils; import org.eclipse.core.runtime.IProduct; import org.eclipse.core.runtime.Platform; import org.jkiss.dbeaver.model.meta.Property; @@ -32,6 +32,12 @@ */ public class WebProductInfo { + private final boolean provideSensitiveInformation; + + public WebProductInfo(boolean provideSensitiveInformation) { + this.provideSensitiveInformation = provideSensitiveInformation; + } + @Property public String getId() { return CommonUtils.notEmpty(Platform.getProduct().getId()); @@ -68,7 +74,9 @@ public String getReleaseTime() { @Property public String getLicenseInfo() { - return CBApplication.getInstance().getInfoDetails(new VoidProgressMonitor()); + return provideSensitiveInformation + ? ServletAppUtils.getServletApplication().getInfoDetails(new VoidProgressMonitor()) + : ""; } @Property @@ -77,4 +85,11 @@ public String getLatestVersionInfo() { return CommonUtils.notEmpty(product.getProperty("versionUpdateURL")); } + @Property + public String getProductPurchaseURL() { + IProduct product = Platform.getProduct(); + return CommonUtils.notEmpty(product.getProperty("productPurchaseURL")); + } + + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java index a717f5bb4e..efad31d740 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,10 +16,12 @@ */ package io.cloudbeaver.model; +import io.cloudbeaver.registry.WebServerFeatureRegistry; import io.cloudbeaver.registry.WebServiceDescriptor; import io.cloudbeaver.registry.WebServiceRegistry; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.server.WebApplication; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.registry.language.PlatformLanguageDescriptor; @@ -29,6 +31,7 @@ import org.jkiss.utils.CommonUtils; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -37,15 +40,16 @@ */ public class WebServerConfig { - private final CBApplication application; + private final WebApplication application; + protected boolean provideSensitiveInformation = true; - public WebServerConfig(CBApplication application) { + public WebServerConfig(@NotNull WebApplication application) { this.application = application; } @Property public String getName() { - return CommonUtils.notEmpty(application.getServerName()); + return CommonUtils.notEmpty(application.getServerConfiguration().getServerName()); } @Property @@ -58,27 +62,6 @@ public String getWorkspaceId() { return DBWorkbench.getPlatform().getWorkspace().getWorkspaceId(); } - @Property - public String getServerURL() { - return CommonUtils.notEmpty(application.getServerURL()); - } - - @Property - public String getRootURI() { - return CommonUtils.notEmpty(application.getRootURI()); - } - - @Deprecated - @Property - public String getHostName() { - return getContainerId(); - } - - @Property - public String getContainerId() { - return CommonUtils.notEmpty(application.getContainerId()); - } - @Property public boolean isAnonymousAccessEnabled() { return application.getAppConfiguration().isAnonymousAccessEnabled(); @@ -89,16 +72,6 @@ public boolean isSupportsCustomConnections() { return application.getAppConfiguration().isSupportsCustomConnections(); } - @Property - public boolean isSupportsConnectionBrowser() { - return application.getAppConfiguration().isSupportsConnectionBrowser(); - } - - @Property - public boolean isSupportsWorkspaces() { - return application.getAppConfiguration().isSupportsUserWorkspaces(); - } - @Property public boolean isPublicCredentialsSaveEnabled() { return application.getAppConfiguration().isPublicCredentialsSaveEnabled(); @@ -120,18 +93,18 @@ public boolean isLicenseValid() { } @Property - public boolean isConfigurationMode() { - return application.isConfigurationMode(); + public String getLicenseStatus() { + return provideSensitiveInformation ? application.getLicenseStatus() : ""; } @Property - public boolean isDevelopmentMode() { - return application.isDevelMode(); + public boolean isConfigurationMode() { + return application.isConfigurationMode(); } @Property - public boolean isRedirectOnFederatedAuth() { - return application.getAppConfiguration().isRedirectOnFederatedAuth(); + public boolean isDevelopmentMode() { + return application.getServerConfiguration().isDevelMode(); } @Property @@ -140,28 +113,32 @@ public boolean isResourceManagerEnabled() { } @Property - public long getSessionExpireTime() { - return application.getConfiguredMaxSessionIdleTime(); + public boolean isSecretManagerEnabled() { + return application.getAppConfiguration().isSecretManagerEnabled(); } @Property - public String getLocalHostAddress() { - return application.getLocalHostAddress(); + public String[] getEnabledFeatures() { + return application.getAppConfiguration().getEnabledFeatures(); } @Property - public String[] getEnabledFeatures() { - return application.getAppConfiguration().getEnabledFeatures(); + @Nullable + public String[] getDisabledBetaFeatures() { + return application.getAppConfiguration().getDisabledBetaFeatures(); } @Property - public String[] getEnabledAuthProviders() { - return application.getAppConfiguration().getEnabledAuthProviders(); + @NotNull + public String[] getServerFeatures() { + return WebServerFeatureRegistry.getInstance().getServerFeatures(); } @Property public WebServerLanguage[] getSupportedLanguages() { - List langs = PlatformLanguageRegistry.getInstance().getLanguages(); + List langs = new ArrayList<>(PlatformLanguageRegistry.getInstance().getLanguages()); + // FIXME: remove hardcoded ordering + langs.sort(Comparator.comparingInt(x -> x.getCode().equals("vi") ? 1 : -1)); WebServerLanguage[] webLangs = new WebServerLanguage[langs.size()]; for (int i = 0; i < webLangs.length; i++) { webLangs[i] = new WebServerLanguage(langs.get(i)); @@ -180,7 +157,7 @@ public WebServiceConfig[] getServices() { @Property public Map getProductConfiguration() { - return CBPlatform.getInstance().getApplication().getProductConfiguration(); + return application.getProductConfiguration(); } @Property @@ -195,7 +172,7 @@ public Map getResourceQuotas() { @Property public WebProductInfo getProductInfo() { - return new WebProductInfo(); + return new WebProductInfo(provideSensitiveInformation); } @Property @@ -208,13 +185,7 @@ public Boolean isDistributed() { return application.isDistributed(); } - @Property - public String getDefaultAuthRole() { - return application.getDefaultAuthRole(); - } - - @Property - public String getDefaultUserTeam() { - return application.getAppConfiguration().getDefaultUserTeam(); + public void setProvideSensitiveInformation(boolean provideSensitiveInformation) { + this.provideSensitiveInformation = provideSensitiveInformation; } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerLanguage.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerLanguage.java index 6eb8321420..e1a2d28f5e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerLanguage.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerLanguage.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServiceConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServiceConfig.java index 87e9e638f4..bf9106f7ab 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServiceConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServiceConfig.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebSettingsGroupInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebSettingsGroupInfo.java new file mode 100644 index 0000000000..a825e3cb09 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebSettingsGroupInfo.java @@ -0,0 +1,43 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.impl.PropertyDescriptor; +import org.jkiss.dbeaver.model.impl.PropertyGroupDescriptor; +import org.jkiss.dbeaver.model.meta.Property; + +public class WebSettingsGroupInfo { + @NotNull + private final PropertyGroupDescriptor groupDescriptor; + + public WebSettingsGroupInfo(@NotNull PropertyGroupDescriptor groupDescriptor) { + this.groupDescriptor = groupDescriptor; + } + + @NotNull + @Property + public String getId() { + return groupDescriptor.getFullId(); + } + + @NotNull + @Property + public String getDisplayName() { + return groupDescriptor.getDisplayName(); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogInfo.java new file mode 100644 index 0000000000..651fb4ba64 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogInfo.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model; + +import org.jkiss.code.NotNull; + +import java.util.List; + +public record WebTransactionLogInfo(@NotNull List transactionLogInfos, int count) { +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogItemInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogItemInfo.java new file mode 100644 index 0000000000..926387a5ea --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogItemInfo.java @@ -0,0 +1,31 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.model; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; + +public record WebTransactionLogItemInfo( + @Nullable Integer id, + @NotNull String time, + @NotNull String type, + @NotNull String queryString, + long durationMs, + long rows, + String result +) { +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/app/WebServerConfiguration.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/app/WebServerConfiguration.java new file mode 100644 index 0000000000..cc5915d588 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/app/WebServerConfiguration.java @@ -0,0 +1,28 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.model.app; + +import org.jkiss.code.Nullable; + +/** + * Web server configuration. + * Contains only server configuration properties. + */ +public interface WebServerConfiguration extends ServletServerConfiguration { + @Nullable + String getServerName(); +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java deleted file mode 100644 index d5f2dee003..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.model.user; - -import io.cloudbeaver.WebServiceUtils; -import io.cloudbeaver.auth.SMAuthProviderFederated; -import io.cloudbeaver.auth.provisioning.SMProvisioner; -import io.cloudbeaver.registry.WebAuthProviderConfiguration; -import io.cloudbeaver.registry.WebAuthProviderDescriptor; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBPlatform; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.security.SMAuthCredentialsProfile; -import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; - -import java.util.ArrayList; -import java.util.List; - -/** - * WebAuthProviderInfo. - */ -public class WebAuthProviderInfo { - - private static final Log log = Log.getLog(WebAuthProviderInfo.class); - - private final WebAuthProviderDescriptor descriptor; - - public WebAuthProviderInfo(WebAuthProviderDescriptor descriptor) { - this.descriptor = descriptor; - } - - WebAuthProviderDescriptor getDescriptor() { - return descriptor; - } - - public String getId() { - return descriptor.getId(); - } - - public String getLabel() { - return descriptor.getLabel(); - } - - public String getIcon() { - return WebServiceUtils.makeIconId(descriptor.getIcon()); - } - - public String getDescription() { - return descriptor.getDescription(); - } - - public boolean isDefaultProvider() { - return descriptor.getId().equals(CBPlatform.getInstance().getApplication().getAppConfiguration().getDefaultAuthProvider()); - } - - public boolean isConfigurable() { - return descriptor.isConfigurable(); - } - - public boolean isFederated() { - return descriptor.getInstance() instanceof SMAuthProviderFederated; - } - - public boolean isTrusted() { - return descriptor.isTrusted(); - } - - public boolean isPrivate() { - return descriptor.isPrivate(); - } - - public boolean isSupportProvisioning() { - return descriptor.getInstance() instanceof SMProvisioner; - } - - public List getConfigurations() { - List result = new ArrayList<>(); - for (SMAuthProviderCustomConfiguration cfg : CBApplication.getInstance().getAppConfiguration().getAuthCustomConfigurations()) { - if (!cfg.isDisabled() && getId().equals(cfg.getProvider())) { - result.add(new WebAuthProviderConfiguration(descriptor, cfg)); - } - } - return result; - } - - public List getCredentialProfiles() { - return descriptor.getCredentialProfiles(); - } - - public String[] getRequiredFeatures() { - String[] rf = descriptor.getRequiredFeatures(); - return rf == null ? new String[0] : rf; - } - - @Override - public String toString() { - return getLabel(); - } - -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebDataSourceProviderInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebDataSourceProviderInfo.java index a749c75e70..e2d0c1e968 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebDataSourceProviderInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebDataSourceProviderInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebDriverRegistry.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebDriverRegistry.java deleted file mode 100644 index c156192b2d..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebDriverRegistry.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.registry; - -import org.eclipse.core.runtime.IConfigurationElement; -import org.eclipse.core.runtime.IExtensionRegistry; -import org.eclipse.core.runtime.Platform; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.connection.DBPDriver; - -import java.util.HashSet; -import java.util.Set; - -public class WebDriverRegistry { - - private static final Log log = Log.getLog(WebDriverRegistry.class); - - private static final String EXTENSION_ID = "io.cloudbeaver.driver"; //$NON-NLS-1$ - private static final String TAG_DRIVER = "driver"; //$NON-NLS-1$ - - private static WebDriverRegistry instance = null; - - public synchronized static WebDriverRegistry getInstance() { - if (instance == null) { - instance = new WebDriverRegistry(); - instance.loadExtensions(Platform.getExtensionRegistry()); - } - return instance; - } - - private final Set webDrivers = new HashSet<>(); - - protected WebDriverRegistry() { - } - - private void loadExtensions(IExtensionRegistry registry) { - { - IConfigurationElement[] extConfigs = registry.getConfigurationElementsFor(EXTENSION_ID); - for (IConfigurationElement ext : extConfigs) { - // Load webServices - if (TAG_DRIVER.equals(ext.getName())) { - this.webDrivers.add(ext.getAttribute("id")); - } - } - } - } - - public boolean isDriverEnabled(DBPDriver driver) { - return webDrivers.contains(driver.getFullId()); - } - -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebHandlerRegistry.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebHandlerRegistry.java index 0a24667538..6772630aad 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebHandlerRegistry.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebHandlerRegistry.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebObjectFeatureProviderDescriptor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebObjectFeatureProviderDescriptor.java new file mode 100644 index 0000000000..a66bf9f8c0 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebObjectFeatureProviderDescriptor.java @@ -0,0 +1,39 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.registry; + +import io.cloudbeaver.service.navigator.DBWFeatureProvider; +import org.eclipse.core.runtime.IConfigurationElement; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.impl.AbstractDescriptor; + +public class WebObjectFeatureProviderDescriptor extends AbstractDescriptor { + + private final String id; + private final DBWFeatureProvider instance; + + public WebObjectFeatureProviderDescriptor(IConfigurationElement cfg) throws DBException { + super(cfg); + this.id = cfg.getAttribute("id"); + ObjectType implClass = new ObjectType(cfg.getAttribute("class")); + this.instance = implClass.createInstance(DBWFeatureProvider.class); + } + + public DBWFeatureProvider getInstance() { + return instance; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebObjectFeatureRegistry.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebObjectFeatureRegistry.java new file mode 100644 index 0000000000..af727ebf64 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebObjectFeatureRegistry.java @@ -0,0 +1,61 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.registry; + +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtensionRegistry; +import org.eclipse.core.runtime.Platform; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; + +import java.util.ArrayList; +import java.util.List; + +public class WebObjectFeatureRegistry { + private static final Log log = Log.getLog(WebObjectFeatureRegistry.class); + private static final String EXTENSION_ID = "io.cloudbeaver.object.feature.provider"; + private static final String TAG_FEATURE_PROVIDER = "featureProvider"; + private static WebObjectFeatureRegistry INSTANCE = null; + + private final List providers = new ArrayList<>(); + + public static synchronized WebObjectFeatureRegistry getInstance() { + if (INSTANCE == null) { + INSTANCE = new WebObjectFeatureRegistry(); + INSTANCE.loadExtensions(Platform.getExtensionRegistry()); + } + return INSTANCE; + } + + private void loadExtensions(IExtensionRegistry registry) { + IConfigurationElement[] extConfigs = registry.getConfigurationElementsFor(EXTENSION_ID); + for (IConfigurationElement ext : extConfigs) { + if (!TAG_FEATURE_PROVIDER.equals(ext.getName())) { + continue; // Only process featureProvider elements + } + try { + providers.add(new WebObjectFeatureProviderDescriptor(ext)); + } catch (DBException e) { + log.error("Error loading object feature provider", e); + } + } + } + + public List getProviders() { + return providers; + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebServletHandlerDescriptor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebServletHandlerDescriptor.java index b4afffce99..a2f48f673f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebServletHandlerDescriptor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebServletHandlerDescriptor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebSessionHandlerDescriptor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebSessionHandlerDescriptor.java index 60fe71e513..1bce58ab05 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebSessionHandlerDescriptor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebSessionHandlerDescriptor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/BaseWebPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/BaseWebPlatform.java new file mode 100644 index 0000000000..c05786881c --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/BaseWebPlatform.java @@ -0,0 +1,112 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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; + +import io.cloudbeaver.server.websockets.WebSocketPingPongJob; +import org.eclipse.core.runtime.Plugin; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.DBConstants; +import org.jkiss.dbeaver.model.app.DBACertificateStorage; +import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.dbeaver.model.impl.app.DefaultCertificateStorage; +import org.jkiss.dbeaver.model.qm.QMRegistry; +import org.jkiss.dbeaver.model.qm.QMUtils; +import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; +import org.jkiss.dbeaver.runtime.SecurityProviderUtils; +import org.jkiss.dbeaver.runtime.qm.QMLogFileWriter; +import org.jkiss.dbeaver.runtime.qm.QMRegistryImpl; + +public abstract class BaseWebPlatform extends BaseServletPlatform { + public static final String TEMP_FILE_FOLDER = "temp-sql-upload-files"; + public static final String TEMP_FILE_IMPORT_FOLDER = "temp-import-files"; + + + private QMRegistryImpl queryManager; + private QMLogFileWriter qmLogWriter; + private DBACertificateStorage certificateStorage; + private ServerGlobalWorkspace workspace; + + @Override + protected synchronized void initialize() { + // Register BC security provider + SecurityProviderUtils.registerSecurityProvider(); + + // Register properties adapter + getApplication().beforeWorkspaceInitialization(); + this.workspace = new ServerGlobalWorkspace(this, getApplication()); + this.workspace.initializeProjects(); + QMUtils.initApplication(this); + + this.queryManager = new QMRegistryImpl(); + + this.qmLogWriter = new QMLogFileWriter(); + this.queryManager.registerMetaListener(qmLogWriter); + + this.certificateStorage = new DefaultCertificateStorage( + WebPlatformActivator.getInstance() + .getStateLocation() + .toFile() + .toPath() + .resolve(DBConstants.CERTIFICATE_STORAGE_FOLDER)); + super.initialize(); + } + + @Override + protected Plugin getProductPlugin() { + return WebPlatformActivator.getInstance(); + } + + @NotNull + @Override + public DBACertificateStorage getCertificateStorage() { + return certificateStorage; + } + + @NotNull + @Override + public DBPWorkspace getWorkspace() { + return workspace; + } + + @NotNull + public abstract WebApplication getApplication(); + + protected void scheduleServerJobs() { + new WebSocketPingPongJob(WebAppUtils.getWebPlatform()).scheduleMonitor(); + } + + @Override + public synchronized void dispose() { + super.dispose(); + if (this.qmLogWriter != null) { + this.queryManager.unregisterMetaListener(qmLogWriter); + this.qmLogWriter.dispose(); + this.qmLogWriter = null; + } + if (this.queryManager != null) { + this.queryManager.dispose(); + //queryManager = null; + } + DataSourceProviderRegistry.dispose(); + } + + @NotNull + public QMRegistry getQueryManager() { + return queryManager; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java deleted file mode 100644 index c67bd507d3..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * 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; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import io.cloudbeaver.model.app.BaseAuthWebAppConfiguration; -import io.cloudbeaver.model.app.WebAuthConfiguration; -import io.cloudbeaver.registry.WebAuthProviderDescriptor; -import io.cloudbeaver.registry.WebAuthProviderRegistry; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; -import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings; -import org.jkiss.utils.CommonUtils; - -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Application configuration - */ -public class CBAppConfig extends BaseAuthWebAppConfiguration implements WebAuthConfiguration { - public static final DataSourceNavigatorSettings.Preset PRESET_WEB = new DataSourceNavigatorSettings.Preset("web", "Web", "Default view"); - - public static final DataSourceNavigatorSettings DEFAULT_VIEW_SETTINGS = PRESET_WEB.getSettings(); - - private boolean supportsCustomConnections; - private boolean supportsConnectionBrowser; - private boolean supportsUserWorkspaces; - private boolean enableReverseProxyAuth; - private boolean forwardProxy; - private boolean publicCredentialsSaveEnabled; - private boolean adminCredentialsSaveEnabled; - private boolean linkExternalCredentialsWithUser; - - private boolean redirectOnFederatedAuth; - private boolean anonymousAccessEnabled; - private boolean grantConnectionsAccessToAnonymousTeam; - @Deprecated - private String anonymousUserRole; - private String anonymousUserTeam; - - private String[] enabledDrivers; - private String[] disabledDrivers; - private DataSourceNavigatorSettings defaultNavigatorSettings; - - private final Map resourceQuotas; - - public CBAppConfig() { - super(); - this.anonymousAccessEnabled = false; - this.anonymousUserRole = DEFAULT_APP_ANONYMOUS_TEAM_NAME; - this.anonymousUserTeam = DEFAULT_APP_ANONYMOUS_TEAM_NAME; - this.supportsCustomConnections = true; - this.supportsConnectionBrowser = false; - this.supportsUserWorkspaces = false; - this.publicCredentialsSaveEnabled = true; - this.adminCredentialsSaveEnabled = true; - this.redirectOnFederatedAuth = false; - this.enabledDrivers = new String[0]; - this.disabledDrivers = new String[0]; - this.defaultNavigatorSettings = DEFAULT_VIEW_SETTINGS; - this.resourceQuotas = new LinkedHashMap<>(); - this.enableReverseProxyAuth = false; - this.forwardProxy = false; - this.linkExternalCredentialsWithUser = true; - this.grantConnectionsAccessToAnonymousTeam = false; - } - - public CBAppConfig(CBAppConfig src) { - super(src); - this.anonymousAccessEnabled = src.anonymousAccessEnabled; - this.anonymousUserRole = src.anonymousUserRole; - this.anonymousUserTeam = src.anonymousUserTeam; - this.supportsCustomConnections = src.supportsCustomConnections; - this.supportsConnectionBrowser = src.supportsConnectionBrowser; - this.supportsUserWorkspaces = src.supportsUserWorkspaces; - this.publicCredentialsSaveEnabled = src.publicCredentialsSaveEnabled; - this.adminCredentialsSaveEnabled = src.adminCredentialsSaveEnabled; - this.redirectOnFederatedAuth = src.redirectOnFederatedAuth; - this.enabledDrivers = src.enabledDrivers; - this.disabledDrivers = src.disabledDrivers; - this.defaultNavigatorSettings = src.defaultNavigatorSettings; - this.resourceQuotas = new LinkedHashMap<>(src.resourceQuotas); - this.enableReverseProxyAuth = src.enableReverseProxyAuth; - this.forwardProxy = src.forwardProxy; - this.linkExternalCredentialsWithUser = src.linkExternalCredentialsWithUser; - this.grantConnectionsAccessToAnonymousTeam = src.grantConnectionsAccessToAnonymousTeam; - } - - @Override - public boolean isAnonymousAccessEnabled() { - return anonymousAccessEnabled; - } - - @Override - public String getAnonymousUserTeam() { - return CommonUtils.notNull(anonymousUserTeam, anonymousUserRole); - } - - public void setAnonymousAccessEnabled(boolean anonymousAccessEnabled) { - this.anonymousAccessEnabled = anonymousAccessEnabled; - } - - public void setResourceManagerEnabled(boolean resourceManagerEnabled) { - this.resourceManagerEnabled = resourceManagerEnabled; - } - - public boolean isSupportsCustomConnections() { - return supportsCustomConnections; - } - - public void setSupportsCustomConnections(boolean supportsCustomConnections) { - this.supportsCustomConnections = supportsCustomConnections; - } - - public boolean isSupportsConnectionBrowser() { - return supportsConnectionBrowser; - } - - public boolean isSupportsUserWorkspaces() { - return supportsUserWorkspaces; - } - - public boolean isPublicCredentialsSaveEnabled() { - return publicCredentialsSaveEnabled; - } - - public void setPublicCredentialsSaveEnabled(boolean publicCredentialsSaveEnabled) { - this.publicCredentialsSaveEnabled = publicCredentialsSaveEnabled; - } - - public boolean isAdminCredentialsSaveEnabled() { - return adminCredentialsSaveEnabled; - } - - public void setAdminCredentialsSaveEnabled(boolean adminCredentialsSaveEnabled) { - this.adminCredentialsSaveEnabled = adminCredentialsSaveEnabled; - } - - public boolean isRedirectOnFederatedAuth() { - return redirectOnFederatedAuth; - } - - public String[] getEnabledDrivers() { - return enabledDrivers; - } - - public void setEnabledDrivers(String[] enabledDrivers) { - this.enabledDrivers = enabledDrivers; - } - - public String[] getDisabledDrivers() { - return disabledDrivers; - } - - public void setDisabledDrivers(String[] disabledDrivers) { - this.disabledDrivers = disabledDrivers; - } - - public String[] getAllAuthProviders() { - return WebAuthProviderRegistry.getInstance().getAuthProviders() - .stream().map(WebAuthProviderDescriptor::getId).toArray(String[]::new); - } - - public DBNBrowseSettings getDefaultNavigatorSettings() { - return defaultNavigatorSettings; - } - - public void setDefaultNavigatorSettings(DBNBrowseSettings defaultNavigatorSettings) { - this.defaultNavigatorSettings = new DataSourceNavigatorSettings(defaultNavigatorSettings); - } - - @NotNull - public Map getPlugins() { - return plugins; - } - - public void setPlugins(@NotNull Map plugins) { - this.plugins.clear(); - this.plugins.putAll(plugins); - } - - public Map getPluginConfig(@NotNull String pluginId) { - return getPluginConfig(pluginId, false); - } - - public T getPluginOptions(@NotNull String pluginId, @NotNull String option, Class theClass) throws DBException { - Map iamSettingsMap = CBPlatform.getInstance().getApplication().getAppConfiguration().getPluginOption( - pluginId, option); - if (CommonUtils.isEmpty(iamSettingsMap)) { - throw new DBException("Settings '" + option + "' not specified in plugin '" + pluginId + "' configuration"); - } - - Gson gson = new GsonBuilder().create(); - return gson.fromJson( - gson.toJsonTree(iamSettingsMap), - theClass); - } - - //////////////////////////////////////////// - // Quotas - - public Map getResourceQuotas() { - return resourceQuotas; - } - - public T getResourceQuota(String quotaId) { - Object quota = resourceQuotas.get(quotaId); - if (quota instanceof String) { - quota = CommonUtils.toDouble(quota); - } - return (T) quota; - } - - public T getResourceQuota(String quotaId, T defaultValue) { - if (resourceQuotas.containsKey(quotaId)) { - return (T) getResourceQuota(quotaId); - } else { - return defaultValue; - } - } - - public boolean isLinkExternalCredentialsWithUser() { - return linkExternalCredentialsWithUser; - } - - - //////////////////////////////////////////// - // Reverse proxy auth - - public boolean isEnabledReverseProxyAuth() { - return enableReverseProxyAuth; - } - - //////////////////////////////////////////// - // Forward proxy - - public boolean isEnabledForwardProxy() { - return forwardProxy; - } - - - public boolean isGrantConnectionsAccessToAnonymousTeam() { - return grantConnectionsAccessToAnonymousTeam; - } - -} 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 deleted file mode 100644 index ace90be81f..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ /dev/null @@ -1,1170 +0,0 @@ -/* - * 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; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.InstanceCreator; -import io.cloudbeaver.WebServiceUtils; -import io.cloudbeaver.auth.CBAuthConstants; -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; -import io.cloudbeaver.service.DBWServiceInitializer; -import io.cloudbeaver.service.DBWServiceServerConfigurator; -import io.cloudbeaver.service.security.SMControllerConfiguration; -import io.cloudbeaver.service.session.WebSessionManager; -import io.cloudbeaver.utils.WebAppUtils; -import io.cloudbeaver.utils.WebDataSourceUtils; -import org.eclipse.core.runtime.Platform; -import org.eclipse.osgi.service.datalocation.Location; -import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.ModelPreferences; -import org.jkiss.dbeaver.model.DBPDataSourceContainer; -import org.jkiss.dbeaver.model.app.DBPApplication; -import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; -import org.jkiss.dbeaver.model.data.json.JSONUtils; -import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.model.security.*; -import org.jkiss.dbeaver.model.security.user.SMObjectPermissions; -import org.jkiss.dbeaver.model.websocket.event.WSEventController; -import org.jkiss.dbeaver.model.websocket.event.WSServerConfigurationChangedEvent; -import org.jkiss.dbeaver.registry.BaseApplicationImpl; -import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings; -import org.jkiss.dbeaver.runtime.DBWorkbench; -import org.jkiss.dbeaver.runtime.IVariableResolver; -import org.jkiss.dbeaver.utils.ContentUtils; -import org.jkiss.dbeaver.utils.GeneralUtils; -import org.jkiss.dbeaver.utils.PrefUtils; -import org.jkiss.dbeaver.utils.SystemVariablesResolver; -import org.jkiss.utils.ArrayUtils; -import org.jkiss.utils.CommonUtils; -import org.jkiss.utils.StandardConstants; - -import java.io.*; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.URL; -import java.net.UnknownHostException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.Permission; -import java.security.Policy; -import java.security.ProtectionDomain; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * This class controls all aspects of the application's execution - */ -public abstract class CBApplication extends BaseWebApplication implements WebAuthApplication { - - private static final Log log = Log.getLog(CBApplication.class); - - private static final boolean RECONFIGURATION_ALLOWED = true; - /** - * In configuration mode sessions expire after a week - */ - private static final long CONFIGURATION_MODE_SESSION_IDLE_TIME = 60 * 60 * 1000 * 24 * 7; - - - static { - Log.setDefaultDebugStream(System.out); - } - - private String staticContent = ""; - - public static CBApplication getInstance() { - return (CBApplication) BaseApplicationImpl.getInstance(); - } - - protected String serverURL; - protected int serverPort = CBConstants.DEFAULT_SERVER_PORT; - private String serverHost = null; - private String serverName = null; - private String contentRoot = CBConstants.DEFAULT_CONTENT_ROOT; - private String rootURI = CBConstants.DEFAULT_ROOT_URI; - private String servicesURI = CBConstants.DEFAULT_SERVICES_URI; - - private String workspaceLocation = CBConstants.DEFAULT_WORKSPACE_LOCATION; - private String driversLocation = CBConstants.DEFAULT_DRIVERS_LOCATION; - private File homeDirectory; - - // Configurations - protected final Map productConfiguration = new HashMap<>(); - protected final Map databaseConfiguration = new HashMap<>(); - protected final SMControllerConfiguration securityManagerConfiguration = new SMControllerConfiguration(); - private final CBAppConfig appConfiguration = new CBAppConfig(); - private Map externalProperties = new LinkedHashMap<>(); - private Map originalConfigurationProperties = new LinkedHashMap<>(); - - // Persistence - protected SMAdminController securityController; - - private long maxSessionIdleTime = CBAuthConstants.MAX_SESSION_IDLE_TIME; - - private boolean develMode = false; - private boolean configurationMode = false; - private boolean enableSecurityManager = false; - private String localHostAddress; - protected String containerId; - private final List localInetAddresses = new ArrayList<>(); - - protected final WSEventController eventController = new WSEventController(); - - private WebSessionManager sessionManager; - - public CBApplication() { - } - - public String getServerURL() { - return serverURL; - } - - // Port this server listens on. If set the 0 a random port is assigned which may be obtained with getLocalPort() - @Override - public int getServerPort() { - return serverPort; - } - - // The network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. - public String getServerHost() { - return serverHost; - } - - public String getServerName() { - return serverName; - } - - public String getContentRoot() { - return contentRoot; - } - - public String getRootURI() { - return rootURI; - } - - public String getServicesURI() { - return servicesURI; - } - - public String getDriversLocation() { - return driversLocation; - } - - public Path getHomeDirectory() { - return homeDirectory.toPath(); - } - - @Override - public boolean isMultiNode() { - return false; - } - - /** - * @return actual max session idle time - */ - public long getMaxSessionIdleTime() { - if (isConfigurationMode()) { - return CONFIGURATION_MODE_SESSION_IDLE_TIME; - } - return maxSessionIdleTime; - } - - /** - * @return max session idle time from server configuration, may differ from {@link #getMaxSessionIdleTime()} - */ - public long getConfiguredMaxSessionIdleTime() { - return maxSessionIdleTime; - } - - public CBAppConfig getAppConfiguration() { - return appConfiguration; - } - - @Override - public WebAuthConfiguration getAuthConfiguration() { - return appConfiguration; - } - - @Override - public String getAuthServiceURL() { - return Stream.of(getServerURL(), getRootURI(), getServicesURI()) - .map(WebAppUtils::removeSideSlashes) - .filter(CommonUtils::isNotEmpty) - .collect(Collectors.joining("/")); - } - - public Map getProductConfiguration() { - return productConfiguration; - } - - public SMAdminController getSecurityController() { - return securityController; - } - - @Override - public boolean isHeadlessMode() { - return true; - } - - @Override - protected void startServer() { - CBPlatform.setApplication(this); - - Path configPath; - try { - configPath = loadServerConfiguration(); - if (configPath == null) { - return; - } - } catch (DBException e) { - log.error(e); - return; - } - - configurationMode = CommonUtils.isEmpty(serverName); - - eventController.setForceSkipEvents(isConfigurationMode()); // do not send events if configuration mode is on - - // Determine address for local host - localHostAddress = System.getenv(CBConstants.VAR_CB_LOCAL_HOST_ADDR); - if (CommonUtils.isEmpty(localHostAddress)) { - localHostAddress = System.getProperty(CBConstants.VAR_CB_LOCAL_HOST_ADDR); - } - if (CommonUtils.isEmpty(localHostAddress) || "127.0.0.1".equals(localHostAddress) || "::0".equals(localHostAddress)) { - localHostAddress = "localhost"; - } - - final Runtime runtime = Runtime.getRuntime(); - initializeAdditionalConfiguration(); - - Location instanceLoc = Platform.getInstanceLocation(); - try { - if (!instanceLoc.isSet()) { - URL wsLocationURL = new URL( - "file", //$NON-NLS-1$ - null, - workspaceLocation); - instanceLoc.set(wsLocationURL, true); - } - } catch (Exception e) { - log.error("Error setting workspace location to " + workspaceLocation, e); - return; - } - - log.debug(GeneralUtils.getProductName() + " " + GeneralUtils.getProductVersion() + " is starting"); //$NON-NLS-1$ - log.debug("\tOS: " + System.getProperty(StandardConstants.ENV_OS_NAME) + " " + System.getProperty(StandardConstants.ENV_OS_VERSION) + " (" + System.getProperty(StandardConstants.ENV_OS_ARCH) + ")"); - log.debug("\tJava version: " + System.getProperty(StandardConstants.ENV_JAVA_VERSION) + " by " + System.getProperty(StandardConstants.ENV_JAVA_VENDOR) + " (" + System.getProperty(StandardConstants.ENV_JAVA_ARCH) + "bit)"); - log.debug("\tInstall path: '" + SystemVariablesResolver.getInstallPath() + "'"); //$NON-NLS-1$ //$NON-NLS-2$ - log.debug("\tGlobal workspace: '" + instanceLoc.getURL() + "'"); //$NON-NLS-1$ //$NON-NLS-2$ - log.debug("\tMemory available " + (runtime.totalMemory() / (1024 * 1024)) + "Mb/" + (runtime.maxMemory() / (1024 * 1024)) + "Mb"); - - DBPApplication application = DBWorkbench.getPlatform().getApplication(); - - log.debug("\tContent root: " + new File(contentRoot).getAbsolutePath()); - log.debug("\tDrivers storage: " + new File(driversLocation).getAbsolutePath()); - //log.debug("\tDrivers root: " + driversLocation); - //log.debug("\tProduct details: " + application.getInfoDetails()); - log.debug("\tListen port: " + serverPort + (CommonUtils.isEmpty(serverHost) ? " on all interfaces" : " on " + serverHost)); - log.debug("\tBase URI: " + servicesURI); - if (develMode) { - log.debug("\tDevelopment mode"); - } else { - log.debug("\tProduction mode"); - } - if (configurationMode) { - log.debug("\tServer is in configuration mode!"); - } - { - determineLocalAddresses(); - log.debug("\tLocal host addresses:"); - for (InetAddress ia : localInetAddresses) { - log.debug("\t\t" + ia.getHostAddress() + " (" + ia.getCanonicalHostName() + ")"); - } - } - { - // Perform services initialization - for (DBWServiceInitializer wsi : WebServiceRegistry.getInstance().getWebServices(DBWServiceInitializer.class)) { - try { - wsi.initializeService(this); - } catch (Exception e) { - log.warn("Error initializing web service " + wsi.getClass().getName(), e); - } - } - - } - - { - try { - initializeSecurityController(); - } catch (Exception e) { - log.error("Error initializing database", e); - return; - } - } - try { - initializeServer(); - } catch (DBException e) { - log.error("Error initializing server", e); - return; - } - - if (configurationMode) { - // Try to configure automatically - performAutoConfiguration(configPath.toFile().getParentFile()); - } else if (!isMultiNode()) { - if (appConfiguration.isGrantConnectionsAccessToAnonymousTeam()) { - grantAnonymousAccessToConnections(appConfiguration, CBConstants.ADMIN_AUTO_GRANT); - } - grantPermissionsToConnections(); - } - - if (enableSecurityManager) { - Policy.setPolicy(new Policy() { - @Override - public boolean implies(ProtectionDomain domain, Permission permission) { - return true; - } - }); - System.setSecurityManager(new SecurityManager()); - } - - eventController.scheduleCheckJob(); - - runWebServer(); - - log.debug("Shutdown"); - - return; - } - - protected void initializeAdditionalConfiguration() { - - } - - /** - * Configures server automatically. - * Called on startup - * - * @param configPath - */ - protected void performAutoConfiguration(File configPath) { - String autoServerName = System.getenv(CBConstants.VAR_AUTO_CB_SERVER_NAME); - String autoServerURL = System.getenv(CBConstants.VAR_AUTO_CB_SERVER_URL); - String autoAdminName = System.getenv(CBConstants.VAR_AUTO_CB_ADMIN_NAME); - String autoAdminPassword = System.getenv(CBConstants.VAR_AUTO_CB_ADMIN_PASSWORD); - - if (CommonUtils.isEmpty(autoServerName) || CommonUtils.isEmpty(autoAdminName) || CommonUtils.isEmpty(autoAdminPassword)) { - // Try to load from auto config file - if (configPath.exists()) { - File autoConfigFile = new File(configPath, CBConstants.AUTO_CONFIG_FILE_NAME); - if (autoConfigFile.exists()) { - Properties autoProps = new Properties(); - try (InputStream is = new FileInputStream(autoConfigFile)) { - autoProps.load(is); - - autoServerName = autoProps.getProperty(CBConstants.VAR_AUTO_CB_SERVER_NAME); - autoServerURL = autoProps.getProperty(CBConstants.VAR_AUTO_CB_SERVER_URL); - autoAdminName = autoProps.getProperty(CBConstants.VAR_AUTO_CB_ADMIN_NAME); - autoAdminPassword = autoProps.getProperty(CBConstants.VAR_AUTO_CB_ADMIN_PASSWORD); - } catch (IOException e) { - log.error("Error loading auto configuration file '" + autoConfigFile.getAbsolutePath() + "'", e); - } - } - } - } - - if (CommonUtils.isEmpty(autoServerName) || CommonUtils.isEmpty(autoAdminName) || CommonUtils.isEmpty(autoAdminPassword)) { - log.info("No auto configuration was found. Server must be configured manually"); - return; - } - try { - finishConfiguration( - autoServerName, - autoServerURL, - autoAdminName, - autoAdminPassword, - Collections.emptyList(), - maxSessionIdleTime, - getAppConfiguration(), - null - ); - } catch (Exception e) { - log.error("Error loading server auto configuration", e); - } - } - - protected void initializeServer() throws DBException { - - } - - private void determineLocalAddresses() { - try { -// InetAddress localHost = InetAddress.getLocalHost(); -// InetAddress[] allMyIps = InetAddress.getAllByName(localHost.getCanonicalHostName()); -// for (InetAddress addr : allMyIps) { -// System.out.println("Local addr: " + addr); -// } - try { - InetAddress dockerAddress = InetAddress.getByName(CBConstants.VAR_HOST_DOCKER_INTERNAL); - localInetAddresses.add(dockerAddress); - log.debug("\tRun in Docker container (" + dockerAddress + ")?"); - } catch (UnknownHostException e) { - // Ignore - not a docker env - } - - boolean hasLoopbackAddress = false; - for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { - NetworkInterface intf = en.nextElement(); - for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { - InetAddress localInetAddress = enumIpAddr.nextElement(); - boolean loopbackAddress = localInetAddress.isLoopbackAddress(); - if (loopbackAddress ? !hasLoopbackAddress : !localInetAddress.isLinkLocalAddress()) { - if (loopbackAddress) { - hasLoopbackAddress = true; - } - localInetAddresses.add(localInetAddress); - } - } - } - } catch (Exception e) { - log.error(e); - } - - } - - @NotNull - private File getRuntimeAppConfigFile() { - return getDataDirectory(true).resolve(CBConstants.RUNTIME_APP_CONFIG_FILE_NAME).toFile(); - } - - @NotNull - private Path getRuntimeProductConfigFilePath() { - return getDataDirectory(false).resolve(CBConstants.RUNTIME_PRODUCT_CONFIG_FILE_NAME); - } - - @NotNull - public Path getDataDirectory(boolean create) { - File dataDir = new File(workspaceLocation, CBConstants.RUNTIME_DATA_DIR_NAME); - if (create && !dataDir.exists()) { - if (!dataDir.mkdirs()) { - log.error("Can't create data directory '" + dataDir.getAbsolutePath() + "'"); - } - } - return dataDir.toPath(); - } - - @Override - public Path getWorkspaceDirectory() { - return Path.of(workspaceLocation); - } - - private void initializeSecurityController() throws DBException { - securityController = createGlobalSecurityController(); - } - - protected abstract SMAdminController createGlobalSecurityController() throws DBException; - - @Nullable - @Override - protected Path loadServerConfiguration() throws DBException { - Path path = super.loadServerConfiguration(); - - File runtimeConfigFile = getRuntimeAppConfigFile(); - if (runtimeConfigFile.exists()) { - log.debug("Runtime configuration [" + runtimeConfigFile.getAbsolutePath() + "]"); - parseConfiguration(runtimeConfigFile); - } - - return path; - } - - @Override - protected void loadConfiguration(Path configPath) throws DBException { - log.debug("Using configuration [" + configPath + "]"); - - if (!Files.exists(configPath)) { - log.error("Configuration file " + configPath + " doesn't exist. Use defaults."); - } else { - parseConfiguration(configPath.toFile()); - } - // Set default preferences - PrefUtils.setDefaultPreferenceValue(ModelPreferences.getPreferences(), ModelPreferences.UI_DRIVERS_HOME, getDriversLocation()); - } - - private void parseConfiguration(File configFile) throws DBException { - Map configProps = readConfiguration(configFile); - parseConfiguration(configProps); - } - - protected void parseConfiguration(Map configProps) throws DBException { - String homeFolder = initHomeFolder(); - - CBAppConfig prevConfig = new CBAppConfig(appConfiguration); - Gson gson = getGson(); - try { - Map serverConfig = JSONUtils.getObject(configProps, "server"); - serverPort = JSONUtils.getInteger(serverConfig, CBConstants.PARAM_SERVER_PORT, serverPort); - serverHost = JSONUtils.getString(serverConfig, CBConstants.PARAM_SERVER_HOST, serverHost); - if (serverConfig.containsKey(CBConstants.PARAM_SERVER_URL)) { - serverURL = JSONUtils.getString(serverConfig, CBConstants.PARAM_SERVER_URL, serverURL); - } else if (serverURL == null) { - String hostName = serverHost; - if (CommonUtils.isEmpty(hostName)) { - hostName = InetAddress.getLocalHost().getHostName(); - } - serverURL = "http://" + hostName + ":" + serverPort; - } - - serverName = JSONUtils.getString(serverConfig, CBConstants.PARAM_SERVER_NAME, serverName); - contentRoot = WebAppUtils.getRelativePath( - JSONUtils.getString(serverConfig, CBConstants.PARAM_CONTENT_ROOT, contentRoot), homeFolder); - rootURI = readRootUri(serverConfig); - servicesURI = JSONUtils.getString(serverConfig, CBConstants.PARAM_SERVICES_URI, servicesURI); - driversLocation = WebAppUtils.getRelativePath( - JSONUtils.getString(serverConfig, CBConstants.PARAM_DRIVERS_LOCATION, driversLocation), homeFolder); - workspaceLocation = WebAppUtils.getRelativePath( - JSONUtils.getString(serverConfig, CBConstants.PARAM_WORKSPACE_LOCATION, workspaceLocation), homeFolder); - - maxSessionIdleTime = JSONUtils.getLong(serverConfig, CBConstants.PARAM_SESSION_EXPIRE_PERIOD, maxSessionIdleTime); - - develMode = JSONUtils.getBoolean(serverConfig, CBConstants.PARAM_DEVEL_MODE, develMode); - enableSecurityManager = JSONUtils.getBoolean(serverConfig, CBConstants.PARAM_SECURITY_MANAGER, enableSecurityManager); - //SM config - gson.fromJson( - gson.toJsonTree(JSONUtils.getObject(serverConfig, CBConstants.PARAM_SM_CONFIGURATION)), - SMControllerConfiguration.class - ); - // App config - Map appConfig = JSONUtils.getObject(configProps, "app"); - validateConfiguration(appConfig); - gson.fromJson(gson.toJsonTree(appConfig), CBAppConfig.class); - - databaseConfiguration.putAll(JSONUtils.getObject(serverConfig, CBConstants.PARAM_DB_CONFIGURATION)); - - readProductConfiguration(serverConfig, gson, homeFolder); - - String staticContentsFile = JSONUtils.getString(serverConfig, CBConstants.PARAM_STATIC_CONTENT); - if (!CommonUtils.isEmpty(staticContentsFile)) { - try { - staticContent = Files.readString(Path.of(staticContentsFile)); - } catch (IOException e) { - log.error("Error reading static contents from " + staticContentsFile, e); - } - } - parseAdditionalConfiguration(configProps); - } catch (IOException | DBException e) { - throw new DBException("Error parsing server configuration", e); - } - - // Backward compatibility: load configs map - appConfiguration.loadLegacyCustomConfigs(); - - // Merge new config with old one - mergeOldConfiguration(prevConfig); - - patchConfigurationWithProperties(productConfiguration); - } - - private String readRootUri(Map serverConfig) { - String uri = JSONUtils.getString(serverConfig, CBConstants.PARAM_ROOT_URI, rootURI); - //slashes are needed to correctly display static resources on ui - if (!uri.endsWith("/")) { - uri = uri + '/'; - } - if (!uri.startsWith("/")) { - uri = '/' + uri; - } - return uri; - } - - protected void mergeOldConfiguration(CBAppConfig prevConfig) { - Map mergedPlugins = Stream.concat( - prevConfig.getPlugins().entrySet().stream(), - appConfiguration.getPlugins().entrySet().stream() - ) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (o, o2) -> o2)); - appConfiguration.setPlugins(mergedPlugins); - - Set mergedAuthProviders = Stream.concat( - prevConfig.getAuthCustomConfigurations().stream(), - appConfiguration.getAuthCustomConfigurations().stream() - ) - .collect(Collectors.toCollection(LinkedHashSet::new)); - appConfiguration.setAuthProvidersConfigurations(mergedAuthProviders); - } - - @NotNull - protected String initHomeFolder() { - String homeFolder = System.getenv(CBConstants.ENV_CB_HOME); - if (CommonUtils.isEmpty(homeFolder)) { - homeFolder = System.getProperty("user.dir"); - } - if (CommonUtils.isEmpty(homeFolder)) { - homeFolder = "."; - } - homeDirectory = new File(homeFolder); - return homeFolder; - } - - protected void validateConfiguration(Map appConfig) throws DBException { - - } - - protected void readProductConfiguration(Map serverConfig, Gson gson, String homeFolder) throws DBException { - String productConfigPath = WebAppUtils.getRelativePath( - JSONUtils.getString( - serverConfig, - CBConstants.PARAM_PRODUCT_CONFIGURATION, - CBConstants.DEFAULT_PRODUCT_CONFIGURATION - ), - homeFolder - ); - - if (!CommonUtils.isEmpty(productConfigPath)) { - File productConfigFile = new File(productConfigPath); - if (!productConfigFile.exists()) { - log.error("Product configuration file not found (" + productConfigFile.getAbsolutePath() + "'"); - } else { - log.debug("Load product configuration from '" + productConfigFile.getAbsolutePath() + "'"); - try (Reader reader = new InputStreamReader(new FileInputStream(productConfigFile), StandardCharsets.UTF_8)) { - productConfiguration.putAll(JSONUtils.parseMap(gson, reader)); - } catch (Exception e) { - throw new DBException("Error reading product configuration", e); - } - } - } - - // Add product config from runtime - File rtConfig = getRuntimeProductConfigFilePath().toFile(); - if (rtConfig.exists()) { - log.debug("Load product runtime configuration from '" + rtConfig.getAbsolutePath() + "'"); - try (Reader reader = new InputStreamReader(new FileInputStream(rtConfig), StandardCharsets.UTF_8)) { - productConfiguration.putAll(JSONUtils.parseMap(gson, reader)); - } catch (Exception e) { - throw new DBException("Error reading product runtime configuration", e); - } - } - } - - protected Map readConnectionsPermissionsConfiguration(Path parentPath) { - String permissionsConfigPath = WebAppUtils.getRelativePath(CBConstants.DEFAULT_DATASOURCE_PERMISSIONS_CONFIGURATION, parentPath); - File permissionsConfigFile = new File(permissionsConfigPath); - if (permissionsConfigFile.exists()) { - log.debug("Load permissions configuration from '" + permissionsConfigFile.getAbsolutePath() + "'"); - try (Reader reader = new InputStreamReader(new FileInputStream(permissionsConfigFile), StandardCharsets.UTF_8)) { - return JSONUtils.parseMap(getGson(), reader); - } catch (Exception e) { - log.error("Error reading permissions configuration", e); - } - } - return null; - } - - protected Map readConfiguration(File configFile) throws DBException { - Map configProps = new LinkedHashMap<>(); - if (configFile.exists()) { - log.debug("Read configuration [" + configFile.getAbsolutePath() + "]"); - // saves original configuration file - this.originalConfigurationProperties.putAll(readConfigurationFile(configFile)); - - configProps.putAll(readConfigurationFile(configFile)); - patchConfigurationWithProperties(configProps); // patch original properties - } - - readAdditionalConfiguration(configProps); - if (configProps.isEmpty()) { - return Map.of(); - } - - Map serverConfig = getServerConfigProps(configProps); - String externalPropertiesFile = JSONUtils.getString(serverConfig, CBConstants.PARAM_EXTERNAL_PROPERTIES); - if (!CommonUtils.isEmpty(externalPropertiesFile)) { - Properties props = new Properties(); - try (InputStream is = Files.newInputStream(Path.of(externalPropertiesFile))) { - props.load(is); - } catch (IOException e) { - log.error("Error loading external properties from " + externalPropertiesFile, e); - } - for (String propName : props.stringPropertyNames()) { - this.externalProperties.put(propName, props.getProperty(propName)); - } - } - - patchConfigurationWithProperties(configProps); // patch again because properties can be changed - return configProps; - } - - public Map readConfigurationFile(File configFile) throws DBException { - try (Reader reader = new InputStreamReader(new FileInputStream(configFile), StandardCharsets.UTF_8)) { - return JSONUtils.parseMap(getGson(), reader); - } catch (Exception e) { - throw new DBException("Error parsing server configuration", e); - } - } - - private Gson getGson() { - // Stupid way to populate existing objects but ok google (https://github.com/google/gson/issues/431) - InstanceCreator appConfigCreator = type -> appConfiguration; - InstanceCreator navSettingsCreator = type -> (DataSourceNavigatorSettings) appConfiguration.getDefaultNavigatorSettings(); - InstanceCreator smConfigCreator = type -> securityManagerConfiguration; - - return new GsonBuilder() - .setLenient() - .registerTypeAdapter(CBAppConfig.class, appConfigCreator) - .registerTypeAdapter(DataSourceNavigatorSettings.class, navSettingsCreator) - .registerTypeAdapter(SMControllerConfiguration.class, smConfigCreator) - .create(); - } - - protected void readAdditionalConfiguration(Map rootConfig) throws DBException { - - } - - protected void parseAdditionalConfiguration(Map serverConfig) throws DBException { - - } - - private void runWebServer() { - log.debug("Starting Jetty server (" + serverPort + " on " + (CommonUtils.isEmpty(serverHost) ? "all interfaces" : serverHost) + ") "); - new CBJettyServer(this).runServer(); - } - - - @Override - public void stop() { - shutdown(); - } - - protected void shutdown() { - log.debug("Cloudbeaver Server is stopping"); //$NON-NLS-1$ - } - - @Override - public String getInfoDetails(DBRProgressMonitor monitor) { - return ""; - } - - @Override - public String getDefaultProjectName() { - return "GlobalConfiguration"; - } - - public boolean isDevelMode() { - return develMode; - } - - public boolean isConfigurationMode() { - return configurationMode; - } - - public String getLocalHostAddress() { - return localHostAddress; - } - - public boolean isLocalInetAddress(String hostName) { - for (InetAddress addr : localInetAddresses) { - if (addr.getHostAddress().equals(hostName)) { - return true; - } - } - return false; - } - - public List getLocalInetAddresses() { - return localInetAddresses; - } - - public synchronized void finishConfiguration( - @NotNull String newServerName, - @NotNull String newServerURL, - @NotNull String adminName, - @Nullable String adminPassword, - @NotNull List authInfoList, - long sessionExpireTime, - @NotNull CBAppConfig appConfig, - @Nullable SMCredentialsProvider credentialsProvider - ) throws DBException { - if (!RECONFIGURATION_ALLOWED && !isConfigurationMode()) { - throw new DBException("Application must be in configuration mode"); - } - - if (isConfigurationMode()) { - finishSecurityServiceConfiguration(adminName, adminPassword, authInfoList); - } - - // Save runtime configuration - log.debug("Saving runtime configuration"); - saveRuntimeConfig(newServerName, newServerURL, sessionExpireTime, appConfig, credentialsProvider); - - // Grant permissions to predefined connections - if (appConfig.isGrantConnectionsAccessToAnonymousTeam()) { - grantAnonymousAccessToConnections(appConfig, adminName); - } - - // Re-load runtime configuration - try { - log.debug("Reloading application configuration"); - Map runtimeConfigProps = readRuntimeConfigurationProperties(); - if (!runtimeConfigProps.isEmpty()) { - parseConfiguration(runtimeConfigProps); - } - } catch (Exception e) { - throw new DBException("Error parsing configuration", e); - } - - configurationMode = CommonUtils.isEmpty(serverName); - - // Reloading configuration by services - for (DBWServiceServerConfigurator wsc : WebServiceRegistry.getInstance().getWebServices(DBWServiceServerConfigurator.class)) { - try { - wsc.reloadConfiguration(appConfig); - } catch (Exception e) { - log.warn("Error reloading configuration by web service " + wsc.getClass().getName(), e); - } - } - - String sessionId = null; - if (credentialsProvider != null && credentialsProvider.getActiveUserCredentials() != null) { - sessionId = credentialsProvider.getActiveUserCredentials().getSmSessionId(); - } - eventController.addEvent(new WSServerConfigurationChangedEvent(sessionId, null)); - eventController.setForceSkipEvents(isConfigurationMode()); - } - - protected Map readRuntimeConfigurationProperties() throws DBException { - File runtimeConfigFile = getRuntimeAppConfigFile(); - return readConfiguration(runtimeConfigFile); - } - - protected abstract void finishSecurityServiceConfiguration( - @NotNull String adminName, - @Nullable String adminPassword, - @NotNull List authInfoList - ) throws DBException; - - public synchronized void flushConfiguration(SMCredentialsProvider credentialsProvider) throws DBException { - saveRuntimeConfig(serverName, serverURL, maxSessionIdleTime, appConfiguration, credentialsProvider); - } - - - private void grantAnonymousAccessToConnections(CBAppConfig appConfig, String adminName) { - try { - String anonymousTeamId = appConfig.getAnonymousUserTeam(); - var securityController = getSecurityController(); - for (DBPDataSourceContainer ds : WebServiceUtils.getGlobalDataSourceRegistry().getDataSources()) { - var datasourcePermissions = securityController.getObjectPermissions(anonymousTeamId, ds.getId(), SMObjectType.datasource); - if (ArrayUtils.isEmpty(datasourcePermissions.getPermissions())) { - securityController.setObjectPermissions( - Set.of(ds.getId()), - SMObjectType.datasource, - Set.of(anonymousTeamId), - Set.of(SMConstants.DATA_SOURCE_ACCESS_PERMISSION), - adminName - ); - } - } - } catch (Exception e) { - log.error("Error granting anonymous access to connections", e); - } - } - - private void grantPermissionsToConnections() { - try { - var globalRegistry = WebDataSourceUtils.getGlobalDataSourceRegistry(); - var permissionsConfiguration = readConnectionsPermissionsConfiguration(globalRegistry.getProject().getMetadataFolder(false)); - if (permissionsConfiguration == null) { - return; - } - for (var entry : permissionsConfiguration.entrySet()) { - var dataSourceId = entry.getKey(); - var ds = globalRegistry.getDataSource(dataSourceId); - if (ds == null) { - log.error("Connection " + dataSourceId + " is not found in project " + globalRegistry.getProject().getName()); - } - List permissions = JSONUtils.getStringList(permissionsConfiguration, dataSourceId); - var securityController = getSecurityController(); - securityController.deleteAllObjectPermissions(dataSourceId, SMObjectType.datasource); - securityController.setObjectPermissions( - Set.of(dataSourceId), - SMObjectType.datasource, - new HashSet<>(permissions), - Set.of(SMConstants.DATA_SOURCE_ACCESS_PERMISSION), - CBConstants.ADMIN_AUTO_GRANT - ); - } - } catch (DBException e) { - log.error("Error granting permissions to connections", e); - } - } - - protected void saveRuntimeConfig( - String newServerName, - String newServerURL, - long sessionExpireTime, - CBAppConfig appConfig, - SMCredentialsProvider credentialsProvider - ) throws DBException { - if (newServerName == null) { - throw new DBException("Invalid server configuration, server name cannot be empty"); - } - Map configurationProperties = collectConfigurationProperties(newServerName, - newServerURL, - sessionExpireTime, - appConfig); - validateConfiguration(configurationProperties); - writeRuntimeConfig(configurationProperties); - } - - private void writeRuntimeConfig(Map configurationProperties) throws DBException { - File runtimeConfigFile = getRuntimeAppConfigFile(); - if (runtimeConfigFile.exists()) { - ContentUtils.makeFileBackup(runtimeConfigFile.toPath()); - } - - try (Writer out = new OutputStreamWriter(new FileOutputStream(runtimeConfigFile), StandardCharsets.UTF_8)) { - Gson gson = new GsonBuilder() - .setLenient() - .setPrettyPrinting() - .create(); - gson.toJson(configurationProperties, out); - - } catch (IOException e) { - throw new DBException("Error writing runtime configuration", e); - } - } - - protected Map collectConfigurationProperties( - String newServerName, - String newServerURL, - long sessionExpireTime, - CBAppConfig appConfig - ) { - Map rootConfig = new LinkedHashMap<>(); - { - var serverConfigProperties = new LinkedHashMap(); - var originServerConfig = getServerConfigProps(this.originalConfigurationProperties); // get server properties from original configuration file - rootConfig.put("server", serverConfigProperties); - if (!CommonUtils.isEmpty(newServerName)) { - copyConfigValue(originServerConfig, serverConfigProperties, CBConstants.PARAM_SERVER_NAME, newServerName); - } - if (!CommonUtils.isEmpty(newServerURL)) { - copyConfigValue( - originServerConfig, serverConfigProperties, CBConstants.PARAM_SERVER_URL, newServerURL); - } - if (sessionExpireTime > 0) { - copyConfigValue( - originServerConfig, serverConfigProperties, CBConstants.PARAM_SESSION_EXPIRE_PERIOD, sessionExpireTime); - } - var databaseConfigProperties = new LinkedHashMap(); - Map oldRuntimeDBConfig = JSONUtils.getObject(originServerConfig, CBConstants.PARAM_DB_CONFIGURATION); - if (!CommonUtils.isEmpty(databaseConfiguration) && !isDistributed()) { - for (Map.Entry mp : databaseConfiguration.entrySet()) { - copyConfigValue(oldRuntimeDBConfig, databaseConfigProperties, mp.getKey(), mp.getValue()); - } - serverConfigProperties.put(CBConstants.PARAM_DB_CONFIGURATION, databaseConfigProperties); - } - } - { - var appConfigProperties = new LinkedHashMap(); - Map oldAppConfig = JSONUtils.getObject(this.originalConfigurationProperties, "app"); - rootConfig.put("app", appConfigProperties); - - copyConfigValue( - oldAppConfig, appConfigProperties, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled()); - copyConfigValue( - oldAppConfig, appConfigProperties, "supportsCustomConnections", appConfig.isSupportsCustomConnections()); - copyConfigValue( - oldAppConfig, appConfigProperties, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled()); - copyConfigValue( - oldAppConfig, appConfigProperties, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled()); - copyConfigValue( - oldAppConfig, appConfigProperties, "enableReverseProxyAuth", appConfig.isEnabledReverseProxyAuth()); - copyConfigValue( - oldAppConfig, appConfigProperties, "forwardProxy", appConfig.isEnabledForwardProxy()); - copyConfigValue( - oldAppConfig, appConfigProperties, "linkExternalCredentialsWithUser", appConfig.isLinkExternalCredentialsWithUser()); - copyConfigValue( - oldAppConfig, appConfigProperties, "redirectOnFederatedAuth", appConfig.isRedirectOnFederatedAuth()); - copyConfigValue( - oldAppConfig, appConfigProperties, CBConstants.PARAM_RESOURCE_MANAGER_ENABLED, appConfig.isResourceManagerEnabled()); - copyConfigValue( - oldAppConfig, appConfigProperties, CBConstants.PARAM_SHOW_READ_ONLY_CONN_INFO, appConfig.isShowReadOnlyConnectionInfo()); - copyConfigValue( - oldAppConfig, - appConfigProperties, - CBConstants.PARAM_CONN_GRANT_ANON_ACCESS, - appConfig.isGrantConnectionsAccessToAnonymousTeam()); - - Map resourceQuotas = new LinkedHashMap<>(); - Map originResourceQuotas = JSONUtils.getObject(oldAppConfig, CBConstants.PARAM_RESOURCE_QUOTAS); - for (Map.Entry mp : appConfig.getResourceQuotas().entrySet()) { - copyConfigValue(originResourceQuotas, resourceQuotas, mp.getKey(), mp.getValue()); - } - appConfigProperties.put(CBConstants.PARAM_RESOURCE_QUOTAS, resourceQuotas); - - { - // Save only differences in def navigator settings - DBNBrowseSettings navSettings = appConfig.getDefaultNavigatorSettings(); - var navigatorProperties = new LinkedHashMap(); - appConfigProperties.put("defaultNavigatorSettings", navigatorProperties); - - if (navSettings.isShowSystemObjects() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowSystemObjects()) { - navigatorProperties.put("showSystemObjects", navSettings.isShowSystemObjects()); - } - if (navSettings.isShowUtilityObjects() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowUtilityObjects()) { - navigatorProperties.put("showUtilityObjects", navSettings.isShowUtilityObjects()); - } - if (navSettings.isShowOnlyEntities() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowOnlyEntities()) { - navigatorProperties.put("showOnlyEntities", navSettings.isShowOnlyEntities()); - } - if (navSettings.isMergeEntities() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isMergeEntities()) { - navigatorProperties.put("mergeEntities", navSettings.isMergeEntities()); - } - if (navSettings.isHideFolders() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideFolders()) { - navigatorProperties.put("hideFolders", navSettings.isHideFolders()); - } - if (navSettings.isHideSchemas() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideSchemas()) { - navigatorProperties.put("hideSchemas", navSettings.isHideSchemas()); - } - if (navSettings.isHideVirtualModel() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideVirtualModel()) { - navigatorProperties.put("hideVirtualModel", navSettings.isHideVirtualModel()); - } - } - if (appConfig.getEnabledFeatures() != null) { - appConfigProperties.put("enabledFeatures", Arrays.asList(appConfig.getEnabledFeatures())); - } - if (appConfig.getEnabledAuthProviders() != null) { - appConfigProperties.put("enabledAuthProviders", Arrays.asList(appConfig.getEnabledAuthProviders())); - } - if (appConfig.getEnabledDrivers() != null) { - appConfigProperties.put("enabledDrivers", Arrays.asList(appConfig.getEnabledDrivers())); - } - if (appConfig.getDisabledDrivers() != null) { - appConfigProperties.put("disabledDrivers", Arrays.asList(appConfig.getDisabledDrivers())); - } - - if (!CommonUtils.isEmpty(appConfig.getPlugins())) { - appConfigProperties.put("plugins", appConfig.getPlugins()); - } - if (!CommonUtils.isEmpty(appConfig.getAuthCustomConfigurations())) { - appConfigProperties.put("authConfigurations", appConfig.getAuthCustomConfigurations()); - } - } - return rootConfig; - } - - //////////////////////////////////////////////////////////////////////// - // License management - - @Override - public boolean isLicenseRequired() { - return false; - } - - public boolean isLicenseValid() { - return false; - } - - /** - * - */ - public String getStaticContent() { - return staticContent; - } - - //////////////////////////////////////////////////////////////////////// - // Configuration utils - - private void patchConfigurationWithProperties(Map configProps) { - IVariableResolver varResolver = new SystemVariablesResolver() { - @Override - public String get(String name) { - String propValue = externalProperties.get(name); - if (propValue != null) { - return propValue; - } - return super.get(name); - } - }; - patchConfigurationWithProperties(configProps, varResolver); - } - - public WebSessionManager getSessionManager() { - if (sessionManager == null) { - sessionManager = createSessionManager(); - } - return sessionManager; - } - - protected WebSessionManager createSessionManager() { - return new WebSessionManager(this); - } - - @NotNull - public WebDriverRegistry getDriverRegistry() { - return WebDriverRegistry.getInstance(); - } - - public List getAvailableAuthRoles() { - return List.of(); - } - - // gets info about patterns from original configuration file and saves it to runtime config - private void copyConfigValue(Map oldConfig, Map newConfig, String key, Object defaultValue) { - Object value = oldConfig.get(key); - if (value instanceof Map && defaultValue instanceof Map) { - Map subValue = new LinkedHashMap<>(); - Map oldConfigValue = JSONUtils.getObject(oldConfig, key); - for (Map.Entry entry : oldConfigValue.entrySet()) { - copyConfigValue(oldConfigValue, subValue, entry.getKey(), ((Map) defaultValue).get(entry.getKey())); - } - newConfig.put(key, subValue); - } else { - Object newConfigValue = WebAppUtils.getExtractedValue(oldConfig.get(key), defaultValue); - newConfig.put(key, newConfigValue); - } - } - - @Override - public WSEventController getEventController() { - return eventController; - } - - @Nullable - public String getDefaultAuthRole() { - return null; - } - - public String getContainerId() { - if (containerId == null) { - containerId = System.getenv("HOSTNAME"); - } - return containerId; - } -} 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 deleted file mode 100644 index 456970f70b..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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; - -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; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.DBFileController; -import org.jkiss.dbeaver.model.app.DBPWorkspace; -import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; -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; - -import java.util.List; - -public class CBApplicationCE extends CBApplication { - private static final Log log = Log.getLog(CBApplicationCE.class); - - @Override - public SMController createSecurityController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException { - return new EmbeddedSecurityControllerFactory().createSecurityService( - this, - databaseConfiguration, - credentialsProvider, - securityManagerConfiguration - ); - } - @Override - public SMAdminController getAdminSecurityController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException { - return new EmbeddedSecurityControllerFactory().createSecurityService( - this, - databaseConfiguration, - credentialsProvider, - securityManagerConfiguration - ); - } - - protected SMAdminController createGlobalSecurityController() throws DBException { - return new EmbeddedSecurityControllerFactory().createSecurityService( - this, - databaseConfiguration, - new NoAuthCredentialsProvider(), - securityManagerConfiguration - ); - } - - @Override - public RMController createResourceController(@NotNull SMCredentialsProvider credentialsProvider, - @NotNull DBPWorkspace workspace) throws DBException { - return LocalResourceController.builder(credentialsProvider, workspace, this::getSecurityController).build(); - } - - @NotNull - @Override - public DBFileController createFileController(@NotNull SMCredentialsProvider credentialsProvider) { - return new LocalFileController(DBWorkbench.getPlatform().getWorkspace().getAbsolutePath().resolve(DBFileController.DATA_FOLDER)); - } - - protected void shutdown() { - try { - if (securityController instanceof CBEmbeddedSecurityController) { - ((CBEmbeddedSecurityController) securityController).shutdown(); - } - } catch (Exception e) { - log.error(e); - } - super.shutdown(); - } - - protected void finishSecurityServiceConfiguration( - @NotNull String adminName, - @Nullable String adminPassword, - @NotNull List authInfoList - ) throws DBException { - if (securityController instanceof CBEmbeddedSecurityController) { - ((CBEmbeddedSecurityController) securityController).finishConfiguration(adminName, adminPassword, authInfoList); - } - } -} 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 deleted file mode 100644 index 4c53999c7f..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * 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; - -import io.cloudbeaver.auth.NoAuthCredentialsProvider; -import io.cloudbeaver.server.jobs.SessionStateJob; -import io.cloudbeaver.server.jobs.WebSessionMonitorJob; -import io.cloudbeaver.service.session.WebSessionManager; -import org.eclipse.core.resources.ResourcesPlugin; -import org.eclipse.core.runtime.Platform; -import org.eclipse.core.runtime.Plugin; -import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.DBFileController; -import org.jkiss.dbeaver.model.app.DBACertificateStorage; -import org.jkiss.dbeaver.model.app.DBPWorkspace; -import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor; -import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderRegistry; -import org.jkiss.dbeaver.model.connection.DBPDriver; -import org.jkiss.dbeaver.model.connection.DBPDriverLibrary; -import org.jkiss.dbeaver.model.impl.app.DefaultCertificateStorage; -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.DBRProgressMonitor; -import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; -import org.jkiss.dbeaver.registry.BasePlatformImpl; -import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; -import org.jkiss.dbeaver.runtime.DBWorkbench; -import org.jkiss.dbeaver.runtime.SecurityProviderUtils; -import org.jkiss.dbeaver.runtime.qm.QMLogFileWriter; -import org.jkiss.dbeaver.runtime.qm.QMRegistryImpl; -import org.jkiss.dbeaver.utils.ContentUtils; -import org.jkiss.dbeaver.utils.GeneralUtils; -import org.osgi.framework.Bundle; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** - * CBPlatform - */ -public class CBPlatform extends BasePlatformImpl { - - // The plug-in ID - public static final String PLUGIN_ID = "io.cloudbeaver.server"; //$NON-NLS-1$ - - private static final Log log = Log.getLog(CBPlatform.class); - - public static final String WORK_DATA_FOLDER_NAME = ".work-data"; - - static CBPlatform instance; - - @Nullable - private static CBApplication application = null; - - private Path tempFolder; - - private QMRegistryImpl queryManager; - private QMLogFileWriter qmLogWriter; - private DBACertificateStorage certificateStorage; - private WebGlobalWorkspace workspace; - - private final List applicableDrivers = new ArrayList<>(); - - public static CBPlatform getInstance() { - if (instance == null) { - synchronized (CBPlatform.class) { - if (instance == null) { - // Initialize CB platform - CBPlatform.createInstance(); - } - } - } - return instance; - } - - private static CBPlatform createInstance() { - log.debug("Initializing product: " + GeneralUtils.getProductTitle()); - if (Platform.getProduct() != null) { - Bundle definingBundle = Platform.getProduct().getDefiningBundle(); - if (definingBundle != null) { - log.debug("Host plugin: " + definingBundle.getSymbolicName() + " " + definingBundle.getVersion()); - } else { - log.debug("!!! No product bundle found"); - } - } - - try { - instance = new CBPlatform(); - instance.initialize(); - return instance; - } catch (Throwable e) { - log.error("Error initializing CBPlatform", e); - throw new IllegalStateException("Error initializing CBPlatform", e); - } - } - - public static DBPPreferenceStore getGlobalPreferenceStore() { - return WebPlatformActivator.getInstance().getPreferences(); - } - - private CBPlatform() { - } - - public static void setApplication(CBApplication application) { - CBPlatform.application = application; - } - - @Override - protected void initialize() { - long startTime = System.currentTimeMillis(); - log.info("Initialize web platform..."); - - // Register BC security provider - SecurityProviderUtils.registerSecurityProvider(); - - // Register properties adapter - this.workspace = new WebGlobalWorkspace(this, ResourcesPlugin.getWorkspace()); - this.workspace.initializeProjects(); - - QMUtils.initApplication(this); - this.queryManager = new QMRegistryImpl(); - - this.qmLogWriter = new QMLogFileWriter(); - this.queryManager.registerMetaListener(qmLogWriter); - - this.certificateStorage = new DefaultCertificateStorage( - WebPlatformActivator.getInstance().getStateLocation().toFile().toPath().resolve("security")); - - super.initialize(); - - refreshApplicableDrivers(); - - new WebSessionMonitorJob(this) - .scheduleMonitor(); - - new SessionStateJob(this) - .scheduleMonitor(); - - log.info("Web platform initialized (" + (System.currentTimeMillis() - startTime) + "ms)"); - } - - public synchronized void dispose() { - long startTime = System.currentTimeMillis(); - log.debug("Shutdown Core..."); - - super.dispose(); - - if (this.qmLogWriter != null) { - this.queryManager.unregisterMetaListener(qmLogWriter); - this.qmLogWriter.dispose(); - this.qmLogWriter = null; - } - if (this.queryManager != null) { - this.queryManager.dispose(); - //queryManager = null; - } - DataSourceProviderRegistry.dispose(); - - if (workspace != null) { - try { - workspace.save(new VoidProgressMonitor()); - } catch (DBException ex) { - log.error("Can't save workspace", ex); //$NON-NLS-1$ - } - } - - // Remove temp folder - if (tempFolder != null) { - - if (!ContentUtils.deleteFileRecursive(tempFolder.toFile())) { - log.warn("Can't delete temp folder '" + tempFolder.toAbsolutePath() + "'"); - } - tempFolder = null; - } - - CBPlatform.application = null; - CBPlatform.instance = null; - System.gc(); - log.debug("Shutdown completed in " + (System.currentTimeMillis() - startTime) + "ms"); - } - - @NotNull - @Override - public DBPWorkspace getWorkspace() { - return workspace; - } - - @NotNull - @Override - public CBApplication getApplication() { - return application; - } - - public List getApplicableDrivers() { - return applicableDrivers; - } - - @NotNull - @Override - public DBPDataSourceProviderRegistry getDataSourceProviderRegistry() { - return DataSourceProviderRegistry.getInstance(); - } - - @NotNull - public QMRegistry getQueryManager() { - return queryManager; - } - - @NotNull - @Override - public DBPPreferenceStore getPreferenceStore() { - return WebPlatformActivator.getInstance().getPreferences(); - } - - @NotNull - @Override - public DBACertificateStorage getCertificateStorage() { - return certificateStorage; - } - - @NotNull - public Path getTempFolder(DBRProgressMonitor monitor, String name) { - if (tempFolder == null) { - // Make temp folder - monitor.subTask("Create temp folder"); - tempFolder = workspace.getAbsolutePath().resolve(WORK_DATA_FOLDER_NAME); - } - if (!Files.exists(tempFolder)) { - try { - Files.createDirectories(tempFolder); - } catch (IOException e) { - log.error("Can't create temp directory " + tempFolder, e); - } - } - Path folder = tempFolder.resolve(name); - if (!Files.exists(folder)) { - try { - Files.createDirectories(folder); - } catch (IOException e) { - log.error("Error creating temp folder '" + folder.toAbsolutePath() + "'", e); - } - } - return folder; - } - - @Override - protected Plugin getProductPlugin() { - return WebPlatformActivator.getInstance(); - } - - @Override - public boolean isShuttingDown() { - return false; - } - - public WebSessionManager getSessionManager() { - return application.getSessionManager(); - } - - public void refreshApplicableDrivers() { - this.applicableDrivers.clear(); - - for (DBPDataSourceProviderDescriptor dspd : DataSourceProviderRegistry.getInstance().getEnabledDataSourceProviders()) { - for (DBPDriver driver : dspd.getEnabledDrivers()) { - List libraries = driver.getDriverLibraries(); - { - if (!application.getDriverRegistry().isDriverEnabled(driver)) { - continue; - } - boolean hasAllFiles = true, hasJars = false; - for (DBPDriverLibrary lib : libraries) { - if (!DBWorkbench.isDistributed() && !lib.isOptional() && lib.getType() != DBPDriverLibrary.FileType.license && - (lib.getLocalFile() == null || !Files.exists(lib.getLocalFile()))) - { - hasAllFiles = false; - log.error("\tDriver '" + driver.getId() + "' is missing library '" + lib.getDisplayName() + "'"); - } else { - if (lib.getType() == DBPDriverLibrary.FileType.jar) { - hasJars = true; - } - } - } - if (hasAllFiles || hasJars) { - applicableDrivers.add(driver); - } - } - } - } - log.info("Available drivers: " + applicableDrivers.stream().map(DBPDriver::getFullName).collect(Collectors.joining(","))); - } - - @NotNull - @Override - public DBFileController createFileController() { - return getApplication().createFileController(new NoAuthCredentialsProvider()); - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatformUI.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatformUI.java deleted file mode 100644 index b5dd6a93b6..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatformUI.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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; - -import org.jkiss.dbeaver.runtime.ui.console.ConsoleUserInterface; - -/** - * The activator class controls the plug-in life cycle - */ -public class CBPlatformUI extends ConsoleUserInterface { - - public static final CBPlatformUI INSTANCE = new CBPlatformUI(); - -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/ConfigurationUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/ConfigurationUtils.java deleted file mode 100644 index e620d8f576..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/ConfigurationUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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; - -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.connection.DBPDriver; -import org.jkiss.utils.ArrayUtils; - -public class ConfigurationUtils { - private ConfigurationUtils() { - } - - public static boolean isDriverEnabled(@NotNull DBPDriver driver) { - var driverId = driver.getFullId(); - - String[] disabledDrivers = CBApplication.getInstance().getAppConfiguration().getDisabledDrivers(); - if (ArrayUtils.contains(disabledDrivers, driverId)) { - return false; - } - String[] enabledDrivers = CBApplication.getInstance().getAppConfiguration().getEnabledDrivers(); - if (enabledDrivers.length > 0 && !ArrayUtils.contains(enabledDrivers, driverId)) { - return false; - } - - return true; - } - -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/ServletPlatformUI.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/ServletPlatformUI.java new file mode 100644 index 0000000000..afdd674382 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/ServletPlatformUI.java @@ -0,0 +1,32 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import org.jkiss.dbeaver.runtime.ui.console.ConsoleUserInterface; + +/** + * The activator class controls the plug-in life cycle + */ +public class ServletPlatformUI extends ConsoleUserInterface { + + public static final ServletPlatformUI INSTANCE = new ServletPlatformUI(); + + protected void initialize() { + // just a placeholder for injection + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppSessionManager.java new file mode 100644 index 0000000000..75184af3de --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppSessionManager.java @@ -0,0 +1,73 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebHeadlessSession; +import io.cloudbeaver.model.session.WebHttpRequestInfo; +import io.cloudbeaver.model.session.WebSession; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; + +import java.util.Collection; + +public interface WebAppSessionManager { + BaseWebSession closeSession(@NotNull HttpServletRequest request); + + BaseWebSession closeSession(@NotNull String sessionId); + + @NotNull + WebSession getWebSession( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response + ) throws DBWebException; + + @NotNull + WebSession getWebSession( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + boolean errorOnNoFound + ) throws DBWebException; + + @Nullable + BaseWebSession getSession(@NotNull String sessionId); + + @Nullable + WebSession findWebSession(HttpServletRequest request); + + WebSession findWebSession(HttpServletRequest request, boolean errorOnNoFound) throws DBWebException; + + Collection getAllActiveSessions(); + + WebSession getOrRestoreWebSession(WebHttpRequestInfo httpRequest); + + WebHeadlessSession getHeadlessSession( + @Nullable String smAccessToken, + @NotNull WebHttpRequestInfo requestInfo, + boolean create + ) throws DBException; + + boolean touchSession(HttpServletRequest request, HttpServletResponse response) throws DBWebException; + + default void expireIdleSessions() { + + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppUtils.java new file mode 100644 index 0000000000..c062fad708 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppUtils.java @@ -0,0 +1,29 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import org.jkiss.dbeaver.runtime.DBWorkbench; + +public class WebAppUtils { + public static WebApplication getWebApplication() { + return (WebApplication) DBWorkbench.getPlatform().getApplication(); + } + + public static BaseWebPlatform getWebPlatform() { + return (BaseWebPlatform) DBWorkbench.getPlatform(); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebApplication.java new file mode 100644 index 0000000000..f6d973c029 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebApplication.java @@ -0,0 +1,58 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import io.cloudbeaver.model.WebServerConfig; +import io.cloudbeaver.model.app.ServletApplication; +import io.cloudbeaver.model.app.WebAppConfiguration; +import io.cloudbeaver.model.app.WebServerConfiguration; +import io.cloudbeaver.registry.WebDriverRegistry; +import io.cloudbeaver.service.ConnectionController; +import org.jkiss.code.NotNull; + +import java.net.InetAddress; +import java.util.List; +import java.util.Map; + +/** + * Base interface for applications with web ui + */ +public interface WebApplication extends ServletApplication { + WebServerConfiguration getServerConfiguration(); + + WebAppSessionManager getSessionManager(); + + WebDriverRegistry getDriverRegistry(); + + WebAppConfiguration getAppConfiguration(); + + @NotNull + Map getProductConfiguration(); + + List getLocalInetAddresses(); + + Map getInitActions(); + + boolean isLicenseValid(); + + String getLicenseStatus(); + + WebServerConfig getWebServerConfig(); + + ConnectionController getConnectionController(); + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebPlatformAdapterFactory.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebPlatformAdapterFactory.java deleted file mode 100644 index 23560b6e20..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebPlatformAdapterFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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; - -import org.eclipse.core.runtime.IAdapterFactory; -import org.jkiss.dbeaver.model.app.DBPPlatform; -import org.jkiss.dbeaver.runtime.DBWorkbench; -import org.jkiss.dbeaver.runtime.ui.DBPPlatformUI; - -public class WebPlatformAdapterFactory implements IAdapterFactory { - - private static final Class[] CLASSES = new Class[] { DBPPlatform.class, DBPPlatformUI.class }; - - @Override - public T getAdapter(Object adaptableObject, Class adapterType) { - if (adaptableObject instanceof DBWorkbench) { - if (adapterType == DBPPlatform.class) { - return adapterType.cast(CBPlatform.getInstance()); - } else if (adapterType == DBPPlatformUI.class) { - return adapterType.cast(CBPlatformUI.INSTANCE); - } - } - return null; - } - - @Override - public Class[] getAdapterList() { - return CLASSES; - } - -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebServiceConnectionsImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebServiceConnectionsImpl.java index 139dbdec1f..42616fade3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebServiceConnectionsImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebServiceConnectionsImpl.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java index caa30fbc55..652b1d53b3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -18,11 +18,12 @@ import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWServletHandler; +import io.cloudbeaver.utils.ServletAppUtils; +import jakarta.servlet.Servlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jkiss.dbeaver.DBException; -import javax.servlet.Servlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Enumeration; import java.util.HashMap; @@ -43,7 +44,7 @@ protected void createActionFromParams(WebSession session, HttpServletRequest req action.saveInSession(session); // Redirect to home - response.sendRedirect("/"); + response.sendRedirect(ServletAppUtils.getServletApplication().getServerConfiguration().getRootURI()); } protected abstract String getActionConsole(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionSessionHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionSessionHandler.java index f0628b93c6..554f0edf07 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionSessionHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionSessionHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -18,10 +18,10 @@ import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWSessionHandler; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jkiss.dbeaver.DBException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; public abstract class AbstractActionSessionHandler implements DBWSessionHandler { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/CBServerAction.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/CBServerAction.java index 157c4a9d53..03db51c316 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/CBServerAction.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/CBServerAction.java @@ -1,18 +1,18 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp + * Copyright (C) 2010-2024 DBeaver Corp and others * - * All Rights Reserved. + * 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 * - * 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. + * 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.actions; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/data/WebGeometryValueSerializer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/data/WebGeometryValueSerializer.java index a12eb55b04..be251c42d5 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/data/WebGeometryValueSerializer.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/data/WebGeometryValueSerializer.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSAbstractProjectEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSAbstractProjectEventHandler.java index 4b6a3801fa..a31ebecfda 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSAbstractProjectEventHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSAbstractProjectEventHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -18,7 +18,6 @@ import io.cloudbeaver.model.session.BaseWebSession; import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.WSProjectEvent; /** @@ -29,7 +28,6 @@ public abstract class WSAbstractProjectEventHandler implements WSEventHand @Override public void handleEvent(@NotNull EVENT event) { log.debug(event.getTopicId() + " event handled"); - Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); + Collection allSessions = WebAppUtils.getWebApplication() + .getSessionManager() + .getAllActiveSessions(); for (var activeUserSession : allSessions) { if (!isAcceptableInSession(activeUserSession, event)) { log.debug("Cannot handle " + event.getTopicId() + " event '" + event.getId() + 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..04fe89f805 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java @@ -0,0 +1,63 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.BaseWebPlatform; +import io.cloudbeaver.server.WebAppUtils; +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.Files; +import java.nio.file.Path; + +public class WSDeleteTempFileHandler implements WSEventHandler { + + private static final Log log = Log.getLog(WSDeleteTempFileHandler.class); + + public void resetTempFolder(String sessionId) { + Path path = WebAppUtils.getWebPlatform() + .getTempFolder(new VoidProgressMonitor(), BaseWebPlatform.TEMP_FILE_FOLDER) + .resolve(sessionId); + if (Files.exists(path)) { + try { + IOUtils.deleteDirectory(path); + } catch (IOException e) { + log.error("Error deleting temp path", e); + } + } + path = WebAppUtils.getWebPlatform() + .getTempFolder(new VoidProgressMonitor(), BaseWebPlatform.TEMP_FILE_IMPORT_FOLDER) + .resolve(sessionId); + if (Files.exists(path)) { + 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/events/WSEventHandlerWorkspaceConfigUpdate.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSEventHandlerWorkspaceConfigUpdate.java new file mode 100644 index 0000000000..6d5f6ecac6 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSEventHandlerWorkspaceConfigUpdate.java @@ -0,0 +1,37 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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 org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.WorkspaceConfigEventManager; +import org.jkiss.dbeaver.model.websocket.event.WSWorkspaceConfigurationChangedEvent; +import org.jkiss.dbeaver.registry.driver.DriverDescriptorSerializerLegacy; + +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(); + log.info("Config file changed: " + configFileName); + WorkspaceConfigEventManager.fireConfigChangedEvent(configFileName); + if (DriverDescriptorSerializerLegacy.DRIVERS_FILE_NAME.equals(event.getConfigFilePath())) { + super.handleEvent(event); + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java index cff32e44ac..7806e116b2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -20,6 +20,7 @@ import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.navigator.DBNModel; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDatasourceFolderEvent; import org.jkiss.utils.CommonUtils; @@ -32,15 +33,19 @@ public class WSFolderUpdatedEventHandlerImpl extends WSAbstractProjectEventHandl @Override protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSDatasourceFolderEvent event) { - if (activeUserSession instanceof WebSession) { - var webSession = (WebSession) activeUserSession; + if (activeUserSession instanceof WebSession webSession) { var project = webSession.getProjectById(event.getProjectId()); if (project == null) { log.debug("Project " + event.getProjectId() + " is not found in session " + webSession.getSessionId()); return; } project.getDataSourceRegistry().refreshConfig(); - webSession.getNavigatorModel().getRoot().getProjectNode(project).getDatabases().refreshChildren(); + DBNModel navigatorModel = webSession.getNavigatorModel(); + if (navigatorModel == null) { + log.debug("Navigator model is not found in session " + webSession.getSessionId()); + return; + } + navigatorModel.getRoot().getProjectNode(project).getDatabases().refreshChildren(); } activeUserSession.addSessionEvent(event); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSLogEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSLogEventHandler.java index cef7816000..015dfefd46 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSLogEventHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSLogEventHandler.java @@ -1,18 +1,18 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp + * Copyright (C) 2010-2024 DBeaver Corp and others * - * All Rights Reserved. + * 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 * - * 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. + * 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; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java deleted file mode 100644 index 729a60eecd..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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.model.session.BaseWebSession; -import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBPlatform; -import io.cloudbeaver.utils.WebAppUtils; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.security.SMObjectPermissionsGrant; -import org.jkiss.dbeaver.model.security.SMObjectType; -import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent; -import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; -import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; -import org.jkiss.dbeaver.model.websocket.event.permissions.WSObjectPermissionEvent; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public class WSObjectPermissionUpdatedEventHandler extends WSDefaultEventHandler { - private static final Log log = Log.getLog(WSObjectPermissionUpdatedEventHandler.class); - - @Override - protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSObjectPermissionEvent event) { - try { - // we have accessible data sources only in web session - if (event.getSmObjectType() == SMObjectType.datasource && !(activeUserSession instanceof WebSession)) { - return; - } - var user = activeUserSession.getUserContext().getUser(); - var objectId = event.getObjectId(); - - var userSubjects = new HashSet<>(Set.of(user.getTeams())); - userSubjects.add(user.getUserId()); - - var smController = CBPlatform.getInstance().getApplication().getSecurityController(); - var shouldBeAccessible = smController.getObjectPermissionGrants(event.getObjectId(), event.getSmObjectType()) - .stream() - .map(SMObjectPermissionsGrant::getSubjectId) - .anyMatch(userSubjects::contains); - boolean isAccessibleNow; - switch (event.getSmObjectType()) { - case project: - var accessibleProjectIds = activeUserSession.getUserContext().getAccessibleProjectIds(); - isAccessibleNow = accessibleProjectIds.contains(objectId); - if (shouldBeAccessible && !isAccessibleNow) { - // adding project to session cache - activeUserSession.addSessionProject(objectId); - activeUserSession.addSessionEvent( - WSProjectUpdateEvent.create( - event.getSessionId(), - event.getUserId(), - objectId - ) - ); - } else if (!shouldBeAccessible && isAccessibleNow) { - // removing project from session cache - activeUserSession.removeSessionProject(objectId); - activeUserSession.addSessionEvent( - WSProjectUpdateEvent.delete( - event.getSessionId(), - event.getUserId(), - objectId - ) - ); - }; - break; - case datasource: - var webSession = (WebSession) activeUserSession; - var project = webSession.getProjectById(WebAppUtils.getGlobalProjectId()); - if (project == null) { - log.error("Project " + WebAppUtils.getGlobalProjectId() + - " is not found in session " + activeUserSession.getSessionId()); - return; - } - isAccessibleNow = webSession.findWebConnectionInfo(objectId) != null; - var dataSources = List.of(objectId); - if (shouldBeAccessible && !isAccessibleNow) { - webSession.addAccessibleConnectionToCache(objectId); - webSession.addSessionEvent( - WSDataSourceEvent.create( - event.getSessionId(), - event.getUserId(), - WebAppUtils.getGlobalProjectId(), - dataSources, - WSDataSourceProperty.CONFIGURATION - ) - ); - } else if (!shouldBeAccessible && isAccessibleNow) { - webSession.removeAccessibleConnectionFromCache(objectId); - webSession.addSessionEvent( - WSDataSourceEvent.delete( - event.getSessionId(), - event.getUserId(), - WebAppUtils.getGlobalProjectId(), - dataSources, - WSDataSourceProperty.CONFIGURATION - ) - ); - } - } - } catch (DBException e) { - log.error("Error on changing permissions for project " + - event.getObjectId() + " in session " + activeUserSession.getSessionId(), e); - } - } - - @Override - protected boolean isAcceptableInSession(@NotNull BaseWebSession activeUserSession, @NotNull WSObjectPermissionEvent event) { - return activeUserSession.getUserContext().getUser() != null && super.isAcceptableInSession(activeUserSession, event); - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSProjectUpdatedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSProjectUpdatedEventHandler.java index 0c854840c3..803531f57e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSProjectUpdatedEventHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSProjectUpdatedEventHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -21,7 +21,6 @@ import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent; public class WSProjectUpdatedEventHandler extends WSAbstractProjectEventHandler { @@ -33,10 +32,10 @@ protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @Not var eventId = event.getId(); var projectId = event.getProjectId(); try { - if (eventId.equals(WSEventType.RM_PROJECT_ADDED.getEventId())) { + if (WSProjectUpdateEvent.ADDED.equals(eventId)) { activeUserSession.addSessionProject(projectId); log.info("Project '" + projectId + "' added to '" + activeUserSession.getSessionId() + "' session"); - } else if (eventId.equals(WSEventType.RM_PROJECT_REMOVED.getEventId())) { + } else if (WSProjectUpdateEvent.REMOVED.equals(eventId)) { activeUserSession.removeSessionProject(projectId); log.info("Project '" + projectId + "' removed from '" + activeUserSession.getSessionId() + "' session"); } @@ -49,7 +48,7 @@ protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @Not @Override protected boolean isAcceptableInSession(@NotNull BaseWebSession activeUserSession, @NotNull WSProjectUpdateEvent event) { return !WSWebUtils.isSessionIdEquals(activeUserSession, event.getSessionId()) && - (event.getId().equals(WSEventType.RM_PROJECT_REMOVED.getEventId()) || + (event.getId().equals(WSProjectUpdateEvent.REMOVED) || activeUserSession.getUserContext().hasPermission(DBWConstants.PERMISSION_ADMIN)); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLBindingContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLBindingContext.java index 70fb9f0fc3..b0e2485d40 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLBindingContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLBindingContext.java @@ -41,6 +41,7 @@ RuntimeWiring buildRuntimeWiring() { runtimeWiring = RuntimeWiring.newRuntimeWiring(); runtimeWiring .scalar(ExtendedScalars.DateTime) + .scalar(ExtendedScalars.Date) .scalar(ExtendedScalars.Object); queryType = TypeRuntimeWiring.newTypeWiring("Query"); mutationType = TypeRuntimeWiring.newTypeWiring("Mutation"); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLConstants.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLConstants.java index b27aadd27a..eea4886363 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLConstants.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLConstants.java @@ -1,78 +1,97 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.graphql; public class GraphQLConstants { public static final String CONTENT_TYPE_JSON_UTF8 = "application/json;charset=UTF-8"; - public static final String SCHEMA_READ_QUERY = " __schema {\n" + - " queryType { name }\n" + - " mutationType { name }\n" + - " subscriptionType { name }\n" + - " types {\n" + - " ...FullType\n" + - " }\n" + - " directives {\n" + - " name\n" + - " description\n" + - " locations\n" + - " args {\n" + - " ...InputValue\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " fragment FullType on __Type {\n" + - " kind\n" + - " name\n" + - " description\n" + - " fields(includeDeprecated: true) {\n" + - " name\n" + - " description\n" + - " args {\n" + - " ...InputValue\n" + - " }\n" + - " type {\n" + - " ...TypeRef\n" + - " }\n" + - " isDeprecated\n" + - " deprecationReason\n" + - " }\n" + - " inputFields {\n" + - " ...InputValue\n" + - " }\n" + - " interfaces {\n" + - " ...TypeRef\n" + - " }\n" + - " enumValues(includeDeprecated: true) {\n" + - " name\n" + - " description\n" + - " isDeprecated\n" + - " deprecationReason\n" + - " }\n" + - " possibleTypes {\n" + - " ...TypeRef\n" + - " }\n" + - " }\n" + - " fragment InputValue on __InputValue {\n" + - " name\n" + - " description\n" + - " type { ...TypeRef }\n" + - " defaultValue\n" + - " }\n" + - " fragment TypeRef on __Type {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " }\n" + - " }\n" + - " }"; + public static final String SCHEMA_READ_QUERY = """ + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + locations + args { + ...InputValue + } + } + } + } + fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + } + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + }\ + """; } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java index c112259e82..efff4a8cff 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -19,33 +19,41 @@ import com.google.gson.*; import graphql.*; import graphql.execution.*; -import graphql.execution.instrumentation.SimplePerformantInstrumentation; +import graphql.execution.instrumentation.Instrumentation; import graphql.language.SourceLocation; import graphql.schema.DataFetchingEnvironment; import graphql.schema.GraphQLSchema; +import graphql.schema.PropertyDataFetcherHelper; import graphql.schema.idl.SchemaGenerator; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.model.apilog.ApiCallInterceptor; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebServiceRegistry; -import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.HttpConstants; +import io.cloudbeaver.server.WebAppUtils; +import io.cloudbeaver.service.DBWBindingContext; import io.cloudbeaver.service.DBWServiceBindingGraphQL; import io.cloudbeaver.service.WebServiceBindingBase; +import io.cloudbeaver.utils.ServletAppUtils; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.InvocationTargetException; -import java.net.URL; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -56,28 +64,29 @@ public class GraphQLEndpoint extends HttpServlet { private static final Log log = Log.getLog(GraphQLEndpoint.class); + private static final boolean DEBUG = true; + private static final String HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; private static final String HEADER_ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; private static final String HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; private static final String CORE_SCHEMA_FILE_NAME = "schema/schema.graphqls"; - - private static final String SESSION_TEMP_COOKIE = "cb-session"; - + public static final String API_PROTOCOL = "GraphQL"; private final GraphQL graphQL; - private static Gson gson = new GsonBuilder() + public static final Gson gson = new GsonBuilder() .serializeNulls() .setPrettyPrinting() .create(); private GraphQLBindingContext bindingContext; - public GraphQLEndpoint() { + public GraphQLEndpoint(Instrumentation instrumentation) { GraphQLSchema schema = buildSchema(); + PropertyDataFetcherHelper.setUseLambdaFactory(false); graphQL = GraphQL .newGraphQL(schema) - .instrumentation(new SimplePerformantInstrumentation()) + .instrumentation(instrumentation) .queryExecutionStrategy(new WebExecutionStrategy()) .mutationExecutionStrategy(new WebExecutionStrategy()) .build(); @@ -115,51 +124,42 @@ private GraphQLSchema buildSchema() { } @Override - protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + protected void doOptions(HttpServletRequest request, HttpServletResponse response) { setDevelHeaders(request, response); } private void setDevelHeaders(HttpServletRequest request, HttpServletResponse response) { - if (CBApplication.getInstance().isDevelMode()) { - // response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - // response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_HEADERS, "*"); - // response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "*"); + if (ServletAppUtils.getServletApplication().getServerConfiguration().isDevelMode()) { - String referrer = request.getHeader("referer"); - - try { - URL url = new URL(referrer); - String protocol = url.getProtocol(); - String host = url.getHost(); - int port = url.getPort(); - - String origin; - - // if the port is not explicitly specified in the input, it will be -1. - if (port == -1) { - origin = String.format("%s://%s", protocol, host); - } else { - origin = String.format("%s://%s:%d", protocol, host, port); - } + String origin = request.getHeader("origin"); + if (origin == null) { + return; + } - // for local machine must be defined explicitly: - response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, origin); - response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_HEADERS, "Set-Cookie, Content-Type"); - response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - } catch (Throwable t) {} + // for local machine must be defined explicitly: + response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, origin); + response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_HEADERS, "Set-Cookie, Content-Type, Authorization"); + response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); } } @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + String contentType = request.getContentType(); + if (CommonUtils.isEmpty(contentType) || !contentType.startsWith(HttpConstants.TYPE_JSON)) { + String error = "Bad request," + (CommonUtils.isEmpty(contentType) + ? " content type is missing" + : " incorrect content type:" + contentType); + response.sendError(400, error); + return; + } String postBody = IOUtils.readToString(request.getReader()); JsonElement json = gson.fromJson(postBody, JsonElement.class); - if (json instanceof JsonArray) { + if (json instanceof JsonArray array) { setDevelHeaders(request, response); response.setContentType(GraphQLConstants.CONTENT_TYPE_JSON_UTF8); response.getWriter().print("[\n"); - JsonArray array = (JsonArray)json; int reqCount = 0; for (int i = 0; i < array.size(); i++) { if (reqCount > 0) { @@ -173,8 +173,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) } response.getWriter().print("\n]"); - } else if (json instanceof JsonObject) { - JsonObject reqObject = (JsonObject) json; + } else if (json instanceof JsonObject reqObject) { executeSingleQuery(request, response, reqObject); } else { response.sendError(400, "Bad JSON request"); @@ -188,20 +187,26 @@ private void executeSingleQuery(HttpServletRequest request, HttpServletResponse return; } JsonElement varJSON = reqObject.get("variables"); - Map variables = varJSON == null ? null : gson.fromJson(varJSON, Map.class); + Map variables = varJSON == null ? null : gson.fromJson(varJSON, JSONUtils.MAP_TYPE_TOKEN); JsonElement operNameJSON = reqObject.get("operationName"); - executeQuery(request, response, query.getAsString(), variables, operNameJSON == null || operNameJSON instanceof JsonNull ? null : operNameJSON.getAsString()); + executeQuery( + request, + response, + query.getAsString(), + variables, + operNameJSON == null || operNameJSON instanceof JsonNull ? null : operNameJSON.getAsString() + ); } @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { String path = request.getPathInfo(); if (path == null) { path = request.getServletPath(); } - boolean develMode = CBApplication.getInstance().isDevelMode(); + boolean develMode = ServletAppUtils.getServletApplication().getServerConfiguration().isDevelMode(); if (path.contentEquals("/schema.json") && develMode) { executeQuery(request, response, GraphQLConstants.SCHEMA_READ_QUERY, null, null); @@ -219,14 +224,21 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } } - private void executeQuery(HttpServletRequest request, HttpServletResponse response, String query, Map variables, String operationName) throws IOException { - GraphQLContext context = new GraphQLContext.Builder() - .of("request", request) - .of("response", response) - .of("bindingContext", bindingContext) - .build(); + private void executeQuery( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + @NotNull String query, + @Nullable Map variables, + @Nullable String operationName + ) throws IOException { + Map mapOfContext = + Map.of( + "request", request, + "response", response, + "bindingContext", bindingContext + ); ExecutionInput.Builder contextBuilder = ExecutionInput.newExecutionInput() - .context(context) + .graphQLContext(mapOfContext) .query(query); if (variables != null) { contextBuilder.variables(variables); @@ -234,45 +246,72 @@ private void executeQuery(HttpServletRequest request, HttpServletResponse respon if (operationName != null) { contextBuilder.operationName(operationName); } - { - String apiCall = operationName; -// if (!CommonUtils.isEmpty(apiCall)) { -// if (variables != null) { -// apiCall += " (" + variables + ")"; -// } -// } - if (apiCall != null) { - log.debug("API > " + apiCall); - } + String sessionId = GraphQLLoggerUtil.getSmSessionId(request); + String userId = GraphQLLoggerUtil.getUserId(request); + String loggerMessage = GraphQLLoggerUtil.buildLoggerMessage(sessionId, userId, variables); + if (operationName != null) { + log.debug("API > " + operationName + loggerMessage); + } else if (DEBUG) { + log.debug("API > " + query + loggerMessage); } + LocalDateTime startTime = LocalDateTime.now(); ExecutionInput executionInput = contextBuilder.build(); - ExecutionResult executionResult = graphQL.execute(executionInput); + ExecutionResult executionResult = null; + Exception executionException = null; + try { + executionResult = graphQL.execute(executionInput); + } catch (Exception e) { + executionException = e; + throw e; + } finally { + String errorMessage = null; + if (executionResult != null && executionResult.getErrors() != null && !executionResult.getErrors().isEmpty()) { + errorMessage = executionResult.getErrors().getFirst().getMessage(); + } else if (executionException != null) { + errorMessage = executionException.getMessage(); + } + if (WebAppUtils.getWebApplication() instanceof ApiCallInterceptor apiCallInterceptor) { + apiCallInterceptor.onApiCallEvent( + request, + variables, + CommonUtils.notEmpty(operationName), userId, startTime, + errorMessage, + API_PROTOCOL + ); + } + } - Map resJSON = executionResult.toSpecification(); - String resString = gson.toJson(resJSON); - setDevelHeaders(request, response); - response.setContentType(GraphQLConstants.CONTENT_TYPE_JSON_UTF8); - response.getWriter().print(resString); + if (executionResult != null) { + Map resJSON = executionResult.toSpecification(); + String resString = gson.toJson(resJSON); + setDevelHeaders(request, response); + response.setContentType(GraphQLConstants.CONTENT_TYPE_JSON_UTF8); + response.getWriter().print(resString); + } } - private class WebExecutionStrategy extends AsyncExecutionStrategy { + + private static class WebExecutionStrategy extends AsyncExecutionStrategy { public WebExecutionStrategy() { super(new WebDataFetcherExceptionHandler()); } } - private class WebDataFetcherExceptionHandler implements DataFetcherExceptionHandler { + private static class WebDataFetcherExceptionHandler implements DataFetcherExceptionHandler { @Override public CompletableFuture handleException(DataFetcherExceptionHandlerParameters handlerParameters) { Throwable exception = handlerParameters.getException(); if (exception instanceof GraphQLException && exception.getCause() != null) { exception = exception.getCause(); } - if (exception instanceof InvocationTargetException) { - exception = ((InvocationTargetException) exception).getTargetException(); + if (exception instanceof InvocationTargetException ite) { + exception = ite.getTargetException(); } - log.debug("GraphQL call failed at '" + handlerParameters.getPath() + "'" /*+ ", " + handlerParameters.getArgumentValues()*/, exception); + log.debug( + "GraphQL call failed at '" + handlerParameters.getPath() + "'" /*+ ", " + handlerParameters.getArgumentValues()*/, + exception + ); // Log in session WebSession webSession = WebServiceBindingBase.findWebSession(handlerParameters.getDataFetchingEnvironment()); @@ -287,18 +326,18 @@ public CompletableFuture handleException(Data if (!(exception instanceof GraphQLError)) { exception = new DBWebException(exception.getMessage(), exception); } - if (exception instanceof DBWebException) { - ((DBWebException) exception).setPath(path.toList()); - ((DBWebException) exception).setLocations(Collections.singletonList(sourceLocation)); + if (exception instanceof DBWebException webException) { + webException.setPath(path.toList()); + webException.setLocations(Collections.singletonList(sourceLocation)); } var result = handlerResult.error((GraphQLError) exception).build(); return CompletableFuture.completedFuture(result); } } - - public static HttpServletRequest getServletRequest(DataFetchingEnvironment env) { - GraphQLContext context = env.getContext(); + @NotNull + public static HttpServletRequest getServletRequestOrThrow(DataFetchingEnvironment env) { + GraphQLContext context = env.getGraphQlContext(); HttpServletRequest request = context.get("request"); if (request == null) { throw new IllegalStateException("Null request"); @@ -307,7 +346,7 @@ public static HttpServletRequest getServletRequest(DataFetchingEnvironment env) } public static HttpServletResponse getServletResponse(DataFetchingEnvironment env) { - GraphQLContext context = env.getContext(); + GraphQLContext context = env.getGraphQlContext(); HttpServletResponse response = context.get("response"); if (response == null) { throw new IllegalStateException("Null response"); @@ -315,9 +354,13 @@ public static HttpServletResponse getServletResponse(DataFetchingEnvironment env return response; } - public static GraphQLBindingContext getBindingContext(DataFetchingEnvironment env) { - GraphQLContext context = env.getContext(); + public static DBWBindingContext getBindingContext(DataFetchingEnvironment env) { + GraphQLContext context = env.getGraphQlContext(); return context.get("bindingContext"); } -} \ No newline at end of file + @NotNull + public GraphQL getGraphQL() { + return graphQL; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLLoggerUtil.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLLoggerUtil.java new file mode 100644 index 0000000000..98655c05bf --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLLoggerUtil.java @@ -0,0 +1,105 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.graphql; + +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.WebAppUtils; +import io.cloudbeaver.server.WebApplication; +import jakarta.servlet.http.HttpServletRequest; +import org.jkiss.code.Nullable; +import org.jkiss.utils.CommonUtils; + +import java.util.Map; +import java.util.Set; + +public class GraphQLLoggerUtil { + + public static final String LOG_API_GRAPHQL_DEBUG_PARAMETER = "log.api.graphql.debug"; + private static final Set PROHIBITED_VARIABLES = + Set.of("password", "config", "parameters", "settings", "licenseText", "credentials", "username"); + + public static String getUserId(HttpServletRequest request) { + WebSession session = getWebSession(request); + if (session == null) { + return null; + } + String userId = session.getUserContext().getUserId(); + if (userId == null && session.getUserContext().isAuthorizedInSecurityManager()) { + return "anonymous"; + } + return userId; + } + + public static String getSmSessionId(HttpServletRequest request) { + WebSession session = getWebSession(request); + if (session == null) { + return null; + } + return session.getUserContext().getSmSessionId(); + } + + @Nullable + public static WebSession getWebSession(HttpServletRequest request) { + if (request.getSession() == null) { + return null; + } + WebApplication webApplication = WebAppUtils.getWebApplication(); + + return webApplication.getSessionManager() + .findWebSession(request); + } + + public static String buildLoggerMessage(String sessionId, String userId, Map variables) { + StringBuilder loggerMessage = new StringBuilder(" [user: ").append(userId) + .append(", sessionId: ").append(sessionId).append("]"); + + if (WebAppUtils.getWebPlatform().getPreferenceStore().getBoolean(LOG_API_GRAPHQL_DEBUG_PARAMETER) + && variables != null + ) { + loggerMessage.append(" [variables] "); + String parsedVariables = parseVarialbes(variables); + if (CommonUtils.isNotEmpty(parsedVariables)) { + loggerMessage.append(parseVarialbes(variables)); + } + } + return loggerMessage.toString(); + } + + private static String parseVarialbes(Map map) { + StringBuilder result = new StringBuilder(); + + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + boolean isProhibited = PROHIBITED_VARIABLES.stream() + .anyMatch(prohibitedKey -> key.toLowerCase().contains(prohibitedKey.toLowerCase())); + + if (isProhibited) { + result.append(key).append(": ").append("******** "); + continue; + } + + if (value instanceof Map) { + result.append(parseVarialbes((Map) value)); + } else { + result.append(key).append(": ").append(value).append(" "); + } + } + return result.toString().trim(); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java deleted file mode 100644 index c3c7f1a257..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * 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.jetty; - -import io.cloudbeaver.registry.WebServiceRegistry; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.graphql.GraphQLEndpoint; -import io.cloudbeaver.server.servlets.CBImageServlet; -import io.cloudbeaver.server.servlets.CBStaticServlet; -import io.cloudbeaver.server.servlets.CBStatusServlet; -import io.cloudbeaver.server.websockets.CBJettyWebSocketManager; -import io.cloudbeaver.service.DBWServiceBindingServlet; -import org.eclipse.jetty.server.*; -import org.eclipse.jetty.server.session.DefaultSessionCache; -import org.eclipse.jetty.server.session.DefaultSessionIdManager; -import org.eclipse.jetty.server.session.FileSessionDataStore; -import org.eclipse.jetty.server.session.SessionHandler; -import org.eclipse.jetty.servlet.ErrorPageErrorHandler; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.servlet.ServletMapping; -import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.runtime.DBWorkbench; -import org.jkiss.dbeaver.utils.GeneralUtils; -import org.jkiss.utils.CommonUtils; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Duration; -import java.util.Arrays; - -public class CBJettyServer { - - private static final Log log = Log.getLog(CBJettyServer.class); - private static final String SESSION_CACHE_DIR = ".http-sessions"; - - static { - // Set Jetty log level to WARN - System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog"); - System.setProperty("org.eclipse.jetty.LEVEL", "WARN"); - } - - private final CBApplication application; - - public CBJettyServer(@NotNull CBApplication application) { - this.application = application; - } - - public void runServer() { - CBApplication application = CBApplication.getInstance(); - try { - JettyServer server; - int serverPort = application.getServerPort(); - String serverHost = application.getServerHost(); - if (CommonUtils.isEmpty(serverHost)) { - server = new JettyServer(serverPort); - } else { - server = new JettyServer( - InetSocketAddress.createUnresolved(serverHost, serverPort)); - } - - { - // Handler configuration - ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); - servletContextHandler.setResourceBase(application.getContentRoot()); - String rootURI = application.getRootURI(); - servletContextHandler.setContextPath(rootURI); - - ServletHolder staticServletHolder = new ServletHolder("static", new CBStaticServlet()); - staticServletHolder.setInitParameter("dirAllowed", "false"); - servletContextHandler.addServlet(staticServletHolder, "/*"); - - ServletHolder imagesServletHolder = new ServletHolder("images", new CBImageServlet()); - servletContextHandler.addServlet(imagesServletHolder, application.getServicesURI() + "images/*"); - - servletContextHandler.addServlet(new ServletHolder("status", new CBStatusServlet()), "/status"); - - servletContextHandler.addServlet(new ServletHolder("graphql", new GraphQLEndpoint()), application.getServicesURI() + "gql/*"); - servletContextHandler.addEventListener(new CBServerContextListener()); - - // Add extensions from services - - CBJettyServletContext servletContext = new CBJettyServletContext(servletContextHandler); - for (DBWServiceBindingServlet wsd : WebServiceRegistry.getInstance().getWebServices(DBWServiceBindingServlet.class)) { - try { - wsd.addServlets(application, servletContext); - } catch (DBException e) { - log.error(e.getMessage(), e); - } - } - - initSessionManager(this.application, servletContextHandler); - - server.setHandler(servletContextHandler); - - var serverConnector = new ServerConnector(server); - server.addConnector(serverConnector); - JettyWebSocketServletContainerInitializer.configure(servletContextHandler, - (context, wsContainer) -> { - wsContainer.setIdleTimeout(Duration.ofMinutes(5)); - // Add websockets - wsContainer.addMapping( - application.getServicesURI() + "ws/*", - new CBJettyWebSocketManager(application.getSessionManager()) - ); - } - ); - ErrorPageErrorHandler errorHandler = new ErrorPageErrorHandler(); - //errorHandler.addErrorPage(404, "/missing.html"); - servletContextHandler.setErrorHandler(errorHandler); - - log.debug("Active servlets:"); //$NON-NLS-1$ - for (ServletMapping sm : servletContextHandler.getServletHandler().getServletMappings()) { - log.debug("\t" + sm.getServletName() + ": " + Arrays.toString(sm.getPathSpecs())); //$NON-NLS-1$ - } - - } - - boolean forwardProxy = application.getAppConfiguration().isEnabledForwardProxy(); - { - // HTTP config - for(Connector y : server.getConnectors()) { - for(ConnectionFactory x : y.getConnectionFactories()) { - if(x instanceof HttpConnectionFactory) { - HttpConfiguration httpConfiguration = ((HttpConnectionFactory)x).getHttpConfiguration(); - httpConfiguration.setSendServerVersion(false); - if (forwardProxy) { - httpConfiguration.addCustomizer(new ForwardedRequestCustomizer()); - } - } - } - } - } - - server.start(); - server.join(); - } catch (Exception e) { - log.error("Error running Jetty server", e); - } - } - - private void initSessionManager( - @NotNull CBApplication application, - @NotNull ServletContextHandler servletContextHandler - ) { - // Init sessions persistence - Path metadataFolder = GeneralUtils.getMetadataFolder(DBWorkbench.getPlatform().getWorkspace().getAbsolutePath()); - Path sessionCacheFolder = metadataFolder.resolve(SESSION_CACHE_DIR); - if (!Files.exists(sessionCacheFolder)) { - try { - Files.createDirectories(sessionCacheFolder); - } catch (IOException e) { - log.error("Can't create http session cache directory '" + sessionCacheFolder.toAbsolutePath() + "'", e); - return; - } - } - - SessionHandler sessionHandler = new SessionHandler()/* { - public HttpCookie access(HttpSession session, boolean secure) { - HttpCookie cookie = getSessionCookie(session, _context == null ? "/" : (_context.getContextPath()), secure); - return cookie; - } - - @Override - public int getRefreshCookieAge() { - // Refresh cookie always (we need it for FA requests) - return 1; - } - }*/; - var maxIdleSeconds = application.getMaxSessionIdleTime(); - int intMaxIdleSeconds; - if (maxIdleSeconds > Integer.MAX_VALUE) { - log.warn("Max session idle time value is greater than Integer.MAX_VALUE. Integer.MAX_VALUE will be used instead"); - intMaxIdleSeconds = Integer.MAX_VALUE; - } else { - intMaxIdleSeconds = (int) maxIdleSeconds; - } - sessionHandler.setMaxInactiveInterval(intMaxIdleSeconds); - - DefaultSessionCache sessionCache = new DefaultSessionCache(sessionHandler); - FileSessionDataStore sessionStore = new FileSessionDataStore(); - - sessionStore.setStoreDir(sessionCacheFolder.toFile()); - sessionCache.setSessionDataStore(sessionStore); - sessionHandler.setSessionCache(sessionCache); - servletContextHandler.setSessionHandler(sessionHandler); - } - - private static class JettyServer extends Server { - public JettyServer(int serverPort) { - super(serverPort); - } - - public JettyServer(InetSocketAddress addr) { - super(addr); - } - - @Override - public void setSessionIdManager(SessionIdManager sessionIdManager) { - if (sessionIdManager instanceof DefaultSessionIdManager) { - // Nullify worker name to avoid dummy prefixes in session ID cookie - ((DefaultSessionIdManager) sessionIdManager).setWorkerName(null); - } - super.setSessionIdManager(sessionIdManager); - } - } -} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java index f91e5a14de..1cb05f98ef 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -18,11 +18,9 @@ package io.cloudbeaver.server.jetty; import io.cloudbeaver.service.DBWServletContext; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.jkiss.dbeaver.DBException; - -import javax.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServlet; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; public class CBJettyServletContext implements DBWServletContext { private final ServletContextHandler contextHandler; @@ -32,7 +30,7 @@ public CBJettyServletContext(ServletContextHandler contextHandler) { } @Override - public void addServlet(String servletId, HttpServlet servlet, String mapping) throws DBException { + public void addServlet(String servletId, HttpServlet servlet, String mapping) { contextHandler.addServlet(new ServletHolder(servletId, servlet), mapping); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyWebSocketContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyWebSocketContext.java new file mode 100644 index 0000000000..dd7330972c --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyWebSocketContext.java @@ -0,0 +1,54 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.jetty; + +import io.cloudbeaver.service.DBWWebSocketContext; +import jakarta.websocket.server.ServerEndpointConfig; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; +import org.eclipse.jetty.server.Server; +import org.jkiss.code.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class CBJettyWebSocketContext implements DBWWebSocketContext { + private final List mappings = new ArrayList<>(); + + private final Server server; + private final ServletContextHandler servletContextHandler; + + public CBJettyWebSocketContext(@NotNull Server server, @NotNull ServletContextHandler servletContextHandler) { + this.server = server; + this.servletContextHandler = servletContextHandler; + } + + + @Override + public void addWebSocket(@NotNull ServerEndpointConfig endpointConfig) { + // Add jakarta.websocket support + JakartaWebSocketServletContainerInitializer.configure(servletContextHandler, (context, container) -> { + container.addEndpoint(endpointConfig); + this.mappings.add(endpointConfig.getPath()); + }); + } + + @NotNull + public List getMappings() { + return mappings; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBServerContextListener.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBServerContextListener.java deleted file mode 100644 index 11f90ab2ed..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBServerContextListener.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.jetty; - -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBConstants; - -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; -import javax.servlet.SessionCookieConfig; - -public class CBServerContextListener implements ServletContextListener { - - // One week - //private static final int CB_SESSION_LIFE_TIME = 60 * 60 * 24 * 7; - - public void contextInitialized(ServletContextEvent sce) { - SessionCookieConfig scf = sce.getServletContext().getSessionCookieConfig(); - - scf.setComment("Cloudbeaver Session ID"); - //scf.setDomain(domain); - //scf.setHttpOnly(httpOnly); - //scf.setMaxAge(CB_SESSION_LIFE_TIME); - scf.setPath(CBApplication.getInstance().getRootURI()); - //scf.setSecure(isSecure); - scf.setName(CBConstants.CB_SESSION_COOKIE_NAME); - } - - public void contextDestroyed(ServletContextEvent sce) { - - } -} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java new file mode 100644 index 0000000000..9b78e53ce5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java @@ -0,0 +1,29 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.jetty; + +import io.cloudbeaver.server.WebApplication; +import org.eclipse.jetty.ee10.servlet.SessionHandler; + +public class CBSessionHandler extends SessionHandler { + static final int ONE_MINUTE = 60; + private final WebApplication application; + + public CBSessionHandler(WebApplication application) { + this.application = application; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSymLinkContentAllowedAliasChecker.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSymLinkContentAllowedAliasChecker.java new file mode 100644 index 0000000000..ecfc108878 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSymLinkContentAllowedAliasChecker.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.jetty; + +import org.eclipse.jetty.server.AliasCheck; +import org.eclipse.jetty.util.resource.Resource; +import org.jkiss.code.NotNull; + +import java.nio.file.Path; + +public class CBSymLinkContentAllowedAliasChecker implements AliasCheck { + @NotNull + private final Path contentRootPath; + + public CBSymLinkContentAllowedAliasChecker(@NotNull Path contentRootPath) { + this.contentRootPath = contentRootPath; + } + + @Override + public boolean checkAlias(String pathInContext, Resource resource) { + Path resourcePath = resource.getPath(); + return resourcePath != null && resourcePath.startsWith(contentRootPath); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/JettyUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/JettyUtils.java new file mode 100644 index 0000000000..8e07f75b8a --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/JettyUtils.java @@ -0,0 +1,59 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.jetty; + +import io.cloudbeaver.server.WebApplication; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.session.DefaultSessionCache; +import org.eclipse.jetty.session.DefaultSessionIdManager; +import org.eclipse.jetty.session.NullSessionDataStore; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; + +public class JettyUtils { + private static final Log log = Log.getLog(JettyUtils.class); + + public static void initSessionManager( + long maxIdleTime, + @NotNull WebApplication application, + @NotNull Server server, + @NotNull ServletContextHandler servletContextHandler + ) { + // Init sessions persistence + CBSessionHandler sessionHandler = new CBSessionHandler(application); + sessionHandler.setRefreshCookieAge(CBSessionHandler.ONE_MINUTE); + int intMaxIdleSeconds; + if (maxIdleTime > Integer.MAX_VALUE) { + log.warn("Max session idle time value is greater than Integer.MAX_VALUE. Integer.MAX_VALUE will be used instead"); + maxIdleTime = Integer.MAX_VALUE; + } + intMaxIdleSeconds = (int) (maxIdleTime / 1000); + log.debug("Max http session idle time: " + intMaxIdleSeconds + "s"); + sessionHandler.setMaxInactiveInterval(intMaxIdleSeconds); + sessionHandler.setMaxCookieAge(intMaxIdleSeconds); + + DefaultSessionCache sessionCache = new DefaultSessionCache(sessionHandler); + sessionCache.setSessionDataStore(new NullSessionDataStore()); + sessionHandler.setSessionCache(sessionCache); + servletContextHandler.setSessionHandler(sessionHandler); + + DefaultSessionIdManager idMgr = new DefaultSessionIdManager(server); + idMgr.setWorkerName(null); + server.addBean(idMgr, true); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java deleted file mode 100644 index 64854916aa..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.jobs; - -import io.cloudbeaver.server.CBPlatform; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.runtime.AbstractJob; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; - -public abstract class PeriodicSystemJob extends AbstractJob { - - @NotNull - protected final CBPlatform platform; - private final long periodMs; - - public PeriodicSystemJob(@NotNull String name, @NotNull CBPlatform platform, long periodMs) { - super(name); - this.platform = platform; - this.periodMs = periodMs; - - setUser(false); - setSystem(true); - } - - @Override - protected IStatus run(@NotNull DBRProgressMonitor monitor) { - if (platform.isShuttingDown()) { - return Status.OK_STATUS; - } - - doJob(monitor); - - // If the platform is still running after the job is completed, reschedule the job - if (!platform.isShuttingDown()) { - scheduleMonitor(); - } - - return Status.OK_STATUS; - } - - protected abstract void doJob(@NotNull DBRProgressMonitor monitor); - - public void scheduleMonitor() { - schedule(periodMs); - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java deleted file mode 100644 index a7451a897b..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.jobs; - -import io.cloudbeaver.server.CBPlatform; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; - -public class SessionStateJob extends PeriodicSystemJob { - private static final Log log = Log.getLog(SessionStateJob.class); - private static final int PERIOD_MS = 30_000; // once per 30 seconds - - public SessionStateJob(@NotNull CBPlatform platform) { - super("Session state sender", platform, PERIOD_MS); - } - - @Override - protected void doJob(@NotNull DBRProgressMonitor monitor) { - try { - platform.getSessionManager().sendSessionsStates(); - } catch (Exception e) { - log.error("Error sending session state", e); - } - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SqlOutputLogReaderJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SqlOutputLogReaderJob.java index b28329e3bf..982218e10a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SqlOutputLogReaderJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SqlOutputLogReaderJob.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -83,31 +83,31 @@ protected IStatus run(DBRProgressMonitor monitor) { private void dumpOutput(DBRProgressMonitor monitor) { if (!monitor.isCanceled()) { - if (dbcServerOutputReader.isAsyncOutputReadSupported()) { - try { - if (!dbcStatement.isStatementClosed()) { - List messages = new ArrayList<>(); - dbcServerOutputReader.readServerOutput(monitor, dbcExecutionContext, null, dbcStatement, new DBCOutputWriter() { - @Override - public void println(@Nullable DBCOutputSeverity severity, @Nullable String message) { - if (message != null && severity != null) { - messages.add(new WSOutputLogInfo(severity.getName(), message)); - } - } - - @Override - public void flush() { - messages.clear(); - } - }); - webSession.addSessionEvent(new WSOutputDBLogEvent( - contextInfoId, - messages, - System.currentTimeMillis())); + List messages = new ArrayList<>(); + final DBCOutputWriter writer = new DBCOutputWriter() { + @Override + public void println(@Nullable DBCOutputSeverity severity, @Nullable String message) { + if (message != null) { + messages.add(new WSOutputLogInfo(severity == null ? null : severity.getName(), message)); } - } catch (DBCException e) { - log.error(e); } + + @Override + public void flush() { + messages.clear(); + } + }; + try { + dbcServerOutputReader.readServerOutput(monitor, dbcExecutionContext, null, null, writer); + if (dbcServerOutputReader.isAsyncOutputReadSupported() && !dbcStatement.isStatementClosed()) { + dbcServerOutputReader.readServerOutput(monitor, dbcExecutionContext, null, dbcStatement, writer); + } + webSession.addSessionEvent(new WSOutputDBLogEvent( + contextInfoId, + messages, + System.currentTimeMillis())); + } catch (DBCException e) { + log.error(e); } } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebDataSourceMonitorJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebDataSourceMonitorJob.java new file mode 100644 index 0000000000..3eb1ab6e1a --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebDataSourceMonitorJob.java @@ -0,0 +1,71 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.jobs; + +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.WebAppSessionManager; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.DBPDataSource; +import org.jkiss.dbeaver.model.app.DBPPlatform; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceDisconnectEvent; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; +import org.jkiss.dbeaver.runtime.jobs.DataSourceMonitorJob; + +import java.util.Collection; +import java.util.List; + +/** + * Web data source monitor job. + */ +public class WebDataSourceMonitorJob extends DataSourceMonitorJob { + private final WebAppSessionManager sessionManager; + + public WebDataSourceMonitorJob( + @NotNull DBPPlatform platform, + @NotNull WebAppSessionManager sessionManager + ) { + super(platform); + this.sessionManager = sessionManager; + } + + @Override + protected void doJob() { + Collection allSessions = sessionManager.getAllActiveSessions(); + allSessions.parallelStream().forEach(s -> { + checkDataSourceAliveInWorkspace(s.getWorkspace(), s.getLastAccessTimeMillis()); + }); + + } + + @Override + protected void showNotification(@NotNull DBPDataSource dataSource) { + final DBPProject project = dataSource.getContainer().getProject(); + if (project.getWorkspaceSession() instanceof WebSession webSession) { + webSession.addSessionEvent( + new WSDataSourceDisconnectEvent( + project.getId(), + dataSource.getContainer().getId(), + webSession.getSessionId(), + webSession.getUserId() + ) + ); + } + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java deleted file mode 100644 index 6622c303d4..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.jobs; - -import io.cloudbeaver.server.CBPlatform; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; - -/** - * WebSessionMonitorJob - */ -public class WebSessionMonitorJob extends PeriodicSystemJob { - private static final Log log = Log.getLog(WebSessionMonitorJob.class); - private static final int MONITOR_INTERVAL = 10000; // once per 10 seconds - - public WebSessionMonitorJob(@NotNull CBPlatform platform) { - super("Web session monitor", platform, MONITOR_INTERVAL); - } - - @Override - protected void doJob(@NotNull DBRProgressMonitor monitor) { - try { - platform.getSessionManager().expireIdleSessions(); - } catch (Exception e) { - log.error("Error on expire idle sessions", e); - } - } -} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/launcher/CBLauncher.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/launcher/CBLauncher.java index 49e7b2f874..26879f0a6f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/launcher/CBLauncher.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/launcher/CBLauncher.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBImageServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBImageServlet.java index 8a931ec078..497c4e87cb 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBImageServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBImageServlet.java @@ -1,19 +1,37 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.servlets; +import io.cloudbeaver.server.CBConstants; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.eclipse.core.runtime.FileLocator; import org.jkiss.dbeaver.Log; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.Locale; public class CBImageServlet extends HttpServlet { @@ -50,12 +68,21 @@ protected void service(HttpServletRequest request, HttpServletResponse response) if (iconURL == null) { iconURL = FileLocator.find(new URL(iconId)); } + if (iconURL == null && "png".equalsIgnoreCase(iconExt)) { + //try to fall back to SVG + iconURL = FileLocator.find(new URL(iconPath + ".svg")); + iconExt = "svg"; + } if (iconURL == null) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Image not found"); return; } - response.setContentType("image/" + iconExt); + String contentType = switch (iconExt.toLowerCase(Locale.ROOT)) { + case "svg" -> "image/svg+xml"; + default -> "image/" + iconExt; + }; + response.setContentType(contentType); setExpireTime(response); // 3 days ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try (InputStream is = new BufferedInputStream(iconURL.openStream())) { @@ -72,9 +99,9 @@ protected void service(HttpServletRequest request, HttpServletResponse response) private void setExpireTime(HttpServletResponse response) { // Http 1.0 header, set a fix expires date. - response.setDateHeader("Expires", System.currentTimeMillis() + CBStaticServlet.STATIC_CACHE_SECONDS * 1000); + response.setDateHeader("Expires", System.currentTimeMillis() + CBConstants.STATIC_CACHE_SECONDS * 1000); // Http 1.1 header, set a time after now. - response.setHeader("Cache-Control", "public, max-age=" + CBStaticServlet.STATIC_CACHE_SECONDS); + response.setHeader("Cache-Control", "public, max-age=" + CBConstants.STATIC_CACHE_SECONDS); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java deleted file mode 100644 index adef60a70e..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * 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.servlets; - -import io.cloudbeaver.DBWConstants; -import io.cloudbeaver.DBWebException; -import io.cloudbeaver.auth.CBAuthConstants; -import io.cloudbeaver.auth.SMAuthProviderFederated; -import io.cloudbeaver.model.session.WebActionParameters; -import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.registry.WebAuthProviderDescriptor; -import io.cloudbeaver.registry.WebAuthProviderRegistry; -import io.cloudbeaver.registry.WebHandlerRegistry; -import io.cloudbeaver.registry.WebServletHandlerDescriptor; -import io.cloudbeaver.server.CBAppConfig; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBPlatform; -import org.eclipse.jetty.http.HttpContent; -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.server.ResourceService; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.util.resource.Resource; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.auth.SMAuthInfo; -import org.jkiss.dbeaver.model.auth.SMAuthProvider; -import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; -import org.jkiss.utils.CommonUtils; -import org.jkiss.utils.IOUtils; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.Enumeration; -import java.util.Map; -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -@WebServlet(urlPatterns = "/") -public class CBStaticServlet extends DefaultServlet { - private static final String AUTO_LOGIN_ACTION = "auto-login"; - private static final String AUTO_LOGIN_AUTH_ID = "auth-id"; - private static final String ACTION = "action"; - public static final int STATIC_CACHE_SECONDS = 60 * 60 * 24 * 3; - - private static final Log log = Log.getLog(CBStaticServlet.class); - - public CBStaticServlet() { - super(makeResourceService()); - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - for (WebServletHandlerDescriptor handler : WebHandlerRegistry.getInstance().getServletHandlers()) { - try { - if (handler.getInstance().handleRequest(this, request, response)) { - return; - } - } catch (DBException e) { - log.warn("Servlet handler '" + handler.getId() + "' failed", e); - } - } - String uri = request.getPathInfo(); - try { - WebSession webSession = CBPlatform.getInstance().getSessionManager().getWebSession( - request, response, false); - performAutoLoginIfNeeded(request, webSession); - WebActionParameters webActionParameters = WebActionParameters.fromSession(webSession, false); - if (CBApplication.getInstance().getAppConfiguration().isRedirectOnFederatedAuth() - && (CommonUtils.isEmpty(uri) || uri.equals("/") || uri.equals("/index.html")) - && request.getParameterMap().isEmpty() - && (webActionParameters == null || !webActionParameters.getParameters().containsValue(AUTO_LOGIN_ACTION)) - ) { - if (processSessionStart(request, response, webSession)) { - return; - } - } - } catch (DBWebException e) { - log.error("Error reading websession", e); - } - super.doGet(request, response); - } - - private void performAutoLoginIfNeeded(HttpServletRequest request, WebSession webSession) { - boolean isAutoLogin = CommonUtils.toBoolean(request.getParameter(CBAuthConstants.CB_AUTO_LOGIN_REQUEST_PARAM)); - if (!isAutoLogin) { - return; - } - - if (webSession.getUserContext().isNonAnonymousUserAuthorizedInSM()) { - log.warn("Auto login failed: user already authorized"); - return; - } - - String authId = request.getParameter(CBAuthConstants.CB_AUTH_ID_REQUEST_PARAM); - if (CommonUtils.isEmpty(authId)) { - log.warn("Auto login failed: authId not found in request"); - return; - } - Map authActionParams = Map.of( - ACTION, AUTO_LOGIN_ACTION, - AUTO_LOGIN_AUTH_ID, authId - ); - WebActionParameters.saveToSession(webSession, authActionParams); - } - - private boolean processSessionStart(HttpServletRequest request, HttpServletResponse response, WebSession webSession) { - CBApplication application = CBApplication.getInstance(); - if (application.isConfigurationMode()) { - return false; - } - CBAppConfig appConfig = application.getAppConfiguration(); - String[] authProviders = appConfig.getEnabledAuthProviders(); - if (authProviders.length == 1) { - String authProviderId = authProviders[0]; - WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(authProviderId); - if (authProvider != null && authProvider.isConfigurable()) { - SMAuthProviderCustomConfiguration activeAuthConfig = null; - for (SMAuthProviderCustomConfiguration cfg : appConfig.getAuthCustomConfigurations()) { - if (!cfg.isDisabled() && cfg.getProvider().equals(authProviderId)) { - if (activeAuthConfig != null) { - return false; - } - activeAuthConfig = cfg; - } - } - if (activeAuthConfig == null) { - return false; - } - - try { - // We have the only provider - // Forward to signon URL - SMAuthProvider authProviderInstance = authProvider.getInstance(); - if (authProviderInstance instanceof SMAuthProviderFederated) { - if (webSession.getUser() == null) { - var securityController = webSession.getSecurityController(); - SMAuthInfo authInfo = securityController.authenticate( - webSession.getSessionId(), - null, - webSession.getSessionParameters(), - WebSession.CB_SESSION_TYPE, - authProvider.getId(), - activeAuthConfig.getId(), - Map.of() - ); - String signInLink = authInfo.getRedirectUrl(); - //ignore current routing if non-root page is open - if (!signInLink.endsWith("#")) { - signInLink += "#"; - } - if (!CommonUtils.isEmpty(signInLink)) { - // Redirect to it - Map authActionParams = Map.of( - ACTION, AUTO_LOGIN_ACTION, - AUTO_LOGIN_AUTH_ID, authInfo.getAuthAttemptId() - ); - WebActionParameters.saveToSession(webSession, authActionParams); - request.getSession().setAttribute(DBWConstants.STATE_ATTR_SIGN_IN_STATE, DBWConstants.SignInState.GLOBAL); - response.sendRedirect(signInLink); - return true; - } - } - } - } catch (Exception e) { - log.debug("Error reading auth provider configuration", e); - } - } - } - - return false; - } - - private static ResourceService makeResourceService() { - ResourceService resourceService = new ProxyResourceService(); - resourceService.setCacheControl(new HttpField(HttpHeader.CACHE_CONTROL, "public, max-age=" + STATIC_CACHE_SECONDS)); - return resourceService; - } - - - private static class ProxyResourceService extends ResourceService { - @Override - protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, HttpContent content, Enumeration reqRanges) throws IOException { - String resourceName = content.getResource().getName(); - if (resourceName.endsWith("index.html") || resourceName.endsWith("sso.html")) { - return patchIndexHtml(response, content); - } - return super.sendData(request, response, include, content, reqRanges); - } - - private boolean patchIndexHtml(HttpServletResponse response, HttpContent content) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Resource resource = content.getResource(); - File file = resource.getFile(); - try (InputStream fis = new FileInputStream(file)) { - IOUtils.copyStream(fis, baos); - } - String indexContents = baos.toString(StandardCharsets.UTF_8); - indexContents = indexContents - .replace("{ROOT_URI}", CBApplication.getInstance().getRootURI()) - .replace("{STATIC_CONTENT}", CBApplication.getInstance().getStaticContent()); - byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); - - putHeaders(response, content, indexBytes.length); - // Disable cache for index.html - response.setHeader(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); - response.setHeader(HttpHeader.EXPIRES.toString(), "0"); - - response.getOutputStream().write(indexBytes); - - return true; - } - } - -} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStatusServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStatusServlet.java deleted file mode 100644 index ffe69190ea..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStatusServlet.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.cloudbeaver.server.servlets; - -import com.google.gson.stream.JsonWriter; -import io.cloudbeaver.server.CBConstants; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.data.json.JSONUtils; -import org.jkiss.dbeaver.utils.GeneralUtils; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; - -@WebServlet(urlPatterns = "/status") -public class CBStatusServlet extends DefaultServlet { - - private static final Log log = Log.getLog(CBStatusServlet.class); - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - response.setContentType(CBConstants.APPLICATION_JSON); - Map infoMap = new LinkedHashMap<>(); - infoMap.put("health", "ok"); - infoMap.put("product.name", GeneralUtils.getProductName()); - infoMap.put("product.version", GeneralUtils.getProductVersion().toString()); - try (JsonWriter writer = new JsonWriter(response.getWriter())) { - JSONUtils.serializeMap(writer, infoMap); - } - } - -} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/WebStatusServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/WebStatusServlet.java new file mode 100644 index 0000000000..e8ee9395b5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/WebStatusServlet.java @@ -0,0 +1,54 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.servlets; + +import com.google.gson.stream.JsonWriter; +import io.cloudbeaver.server.CBConstants; +import io.cloudbeaver.server.WebAppUtils; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.utils.GeneralUtils; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +@WebServlet(urlPatterns = "/status") +public class WebStatusServlet extends DefaultServlet { + + private static final Log log = Log.getLog(WebStatusServlet.class); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType(CBConstants.APPLICATION_JSON); + response.setHeader("Access-Control-Allow-Origin", "*"); + Map infoMap = new LinkedHashMap<>(); + infoMap.put("health", "ok"); + infoMap.put("product.name", GeneralUtils.getProductName()); + infoMap.put("product.version", GeneralUtils.getProductVersion().toString()); + WebAppUtils.getWebApplication().getStatusInfo(infoMap); + try (JsonWriter writer = new JsonWriter(response.getWriter())) { + JSONUtils.serializeMap(writer, infoMap); + } + } + +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java index 26188ed695..413fcdd165 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,37 +17,60 @@ package io.cloudbeaver.server.websockets; import com.google.gson.Gson; -import org.eclipse.jetty.websocket.api.WebSocketAdapter; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Session; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.websocket.WSUtils; import org.jkiss.dbeaver.model.websocket.event.WSEvent; -import java.io.IOException; - -public class CBAbstractWebSocket extends WebSocketAdapter { +public abstract class CBAbstractWebSocket extends Endpoint { private static final Log log = Log.getLog(CBAbstractWebSocket.class); - protected static final Gson gson = WSUtils.gson; + protected static final Gson gson = WSUtils.clientGson; + + @Nullable + private Session webSocketSession; + + @Override + public void onOpen(Session session, EndpointConfig config) { + this.webSocketSession = session; + } public void handleEvent(WSEvent event) { - if (isNotConnected()) { + if (!isOpen()) { return; } try { - getRemote().sendString(gson.toJson(event)); - } catch (IOException e) { + webSocketSession.getBasicRemote().sendText( + gson.toJson(event) + ); + } catch (Exception e) { handleEventException(e); } } - protected void handleEventException(Exception e) { + protected boolean isOpen() { + return webSocketSession != null && webSocketSession.isOpen(); + } + + protected void handleEventException(Throwable e) { log.error("Failed to send websocket message", e); } public void close() { - var session = getSession(); - // the socket may not be connected to the client - if (session != null) { - getSession().close(); + if (isOpen()) { + try { + getSession().close(); + } catch (Exception e) { + log.error("Failed to close websocket", e); + } } } + + @Nullable + public Session getSession() { + return webSocketSession; + } + } 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..924a58fce6 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -18,83 +18,113 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.websocket.CBWebSessionEventHandler; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.WriteCallback; -import org.jkiss.code.NotNull; +import jakarta.websocket.*; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.websocket.event.WSClientEvent; -import org.jkiss.dbeaver.model.websocket.event.WSClientEventType; import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.model.websocket.event.client.WSSessionPingClientEvent; +import org.jkiss.dbeaver.model.websocket.event.client.WSSubscribeOnTopicClientEvent; +import org.jkiss.dbeaver.model.websocket.event.client.WSUnsubscribeFromTopicClientEvent; import org.jkiss.dbeaver.model.websocket.event.client.WSUpdateActiveProjectsClientEvent; +import org.jkiss.dbeaver.model.websocket.event.session.WSAccessTokenExpiredEvent; import org.jkiss.dbeaver.model.websocket.event.session.WSSocketConnectedEvent; +import org.jkiss.utils.CommonUtils; + +import java.time.Duration; public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessionEventHandler { private static final Log log = Log.getLog(CBEventsWebSocket.class); - @NotNull - private final BaseWebSession webSession; - @NotNull - private final WriteCallback callback; + @Nullable + private BaseWebSession webSession; - public CBEventsWebSocket(@NotNull BaseWebSession webSession) { - this.webSession = webSession; + @Override + public void onOpen(Session session, EndpointConfig config) { + super.onOpen(session, config); + if (session.getUserProperties().containsKey(CBWebSocketServerConfigurator.PROP_TOKEN_EXPIRED)) { + handleEvent(new WSAccessTokenExpiredEvent()); + close(); + } else { + this.webSession = (BaseWebSession) session.getUserProperties() + .get(CBWebSocketServerConfigurator.PROP_WEB_SESSION); + this.webSession.addEventHandler(this); + handleEvent(new WSSocketConnectedEvent(webSession.getApplication().getApplicationRunId())); + log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session"); - callback = new WebSocketPingPongCallback(webSession); - } + session.setMaxIdleTimeout(Duration.ofMinutes(5).toMillis()); + session.addMessageHandler(String.class, new FromUserEventHandler()); + session.addMessageHandler(PongMessage.class, new WebSocketPingPongCallback(webSession)); - @Override - public void onWebSocketConnect(Session session) { - super.onWebSocketConnect(session); - this.webSession.addEventHandler(this); - handleEvent(new WSSocketConnectedEvent(webSession.getApplication().getApplicationRunId())); - log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session"); + CBJettyWebSocketManager.registerWebSocket(webSession.getSessionId(), this); + } } - @Override - public void onWebSocketText(String message) { - super.onWebSocketText(message); - var clientEvent = gson.fromJson(message, WSClientEvent.class); - var clientEventType = WSClientEventType.valueById(clientEvent.getId()); - if (clientEventType == null) { - webSession.addSessionError( - new DBWebException("Invalid websocket event: " + message) - ); - return; - } - switch (clientEventType) { - case TOPIC_SUBSCRIBE: { - this.webSession.getEventsFilter().subscribeOnEventTopic(clientEvent.getTopicId()); - break; + private class FromUserEventHandler implements MessageHandler.Whole { + @Override + public void onMessage(String message) { + if (CommonUtils.isEmpty(message)) { + return; + } + if (webSession == null) { + log.warn("No web session for browser event"); + return; } - case TOPIC_UNSUBSCRIBE: { - this.webSession.getEventsFilter().unsubscribeFromEventTopic(clientEvent.getTopicId()); - break; + WSClientEvent clientEvent; + try { + clientEvent = CBAbstractWebSocket.gson.fromJson(message, WSClientEvent.class); + } catch (Exception e) { + if (webSession != null) { + webSession.addSessionError( + new DBWebException("Invalid websocket event: " + e.getMessage()) + ); + } + log.error("Error parsing websocket event: " + e.getMessage(), e); + return; } - case ACTIVE_PROJECTS: { - var projectEvent = (WSUpdateActiveProjectsClientEvent) clientEvent; - this.webSession.getEventsFilter().setSubscribedProjects(projectEvent.getProjectIds()); - break; + if (clientEvent.getId() == null) { + webSession.addSessionError( + new DBWebException("Invalid websocket event: " + message) + ); + return; + } + switch (clientEvent.getId()) { + case WSSubscribeOnTopicClientEvent.ID: { + webSession.getEventsFilter().subscribeOnEventTopic(clientEvent.getTopicId()); + break; + } + case WSUnsubscribeFromTopicClientEvent.ID: { + webSession.getEventsFilter().unsubscribeFromEventTopic(clientEvent.getTopicId()); + break; + } + case WSUpdateActiveProjectsClientEvent.ID: { + var projectEvent = (WSUpdateActiveProjectsClientEvent) clientEvent; + webSession.getEventsFilter().setSubscribedProjects(projectEvent.getProjectIds()); + break; + } + case WSSessionPingClientEvent.ID: { + if (webSession instanceof WebSession session) { + session.updateInfo(true); + } + break; + } + default: + var e = new DBWebException("Unknown websocket client event: " + clientEvent.getId()); + log.error(e.getMessage(), e); + webSession.addSessionError(e); } - default: - var e = new DBWebException("Unknown websocket client event: " + clientEvent.getId()); - log.error(e.getMessage(), e); - webSession.addSessionError(e); } } @Override - public void onWebSocketClose(int statusCode, String reason) { - super.onWebSocketClose(statusCode, reason); - this.webSession.removeEventHandler(this); - log.debug("Socket Closed: [" + statusCode + "] " + reason); - } - - @Override - public void onWebSocketError(Throwable cause) { - super.onWebSocketError(cause); - log.error(cause.getMessage(), cause); - webSession.addSessionError(cause); + public void onClose(Session session, CloseReason closeReason) { + super.onClose(session, closeReason); + log.debug("Socket Closed: [" + closeReason.getCloseCode() + "] " + closeReason.getReasonPhrase()); + if (webSession != null) { + this.webSession.removeEventHandler(this); + } } @Override @@ -103,18 +133,18 @@ public void handleWebSessionEvent(WSEvent event) { } @Override - protected void handleEventException(Exception e) { - super.handleEventException(e); - webSession.addSessionError(e); - } - - @NotNull - public BaseWebSession getWebSession() { - return webSession; + public void onError(Session session, Throwable thr) { + if (webSession != null) { + webSession.addSessionError(thr); + } + log.trace("Error in websocket session: " + thr.getMessage(), thr); } - @NotNull - public WriteCallback getCallback() { - return callback; + @Override + protected void handleEventException(Throwable e) { + super.handleEventException(e); + if (webSession != null) { + webSession.addSessionError(e); + } } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java deleted file mode 100644 index 0cf4091346..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.websockets; - -import org.eclipse.jetty.websocket.api.Session; -import org.jkiss.dbeaver.model.websocket.event.session.WSAccessTokenExpiredEvent; - -public class CBExpiredSessionWebSocket extends CBAbstractWebSocket { - @Override - public void onWebSocketConnect(Session session) { - super.onWebSocketConnect(session); - handleEvent(new WSAccessTokenExpiredEvent()); - close(); - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java index d9ca213bc7..bfe6712f86 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,20 +16,11 @@ */ package io.cloudbeaver.server.websockets; -import io.cloudbeaver.model.session.BaseWebSession; -import io.cloudbeaver.model.session.WebHeadlessSession; -import io.cloudbeaver.server.CBPlatform; -import io.cloudbeaver.service.session.WebSessionManager; -import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; -import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; -import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; +import io.cloudbeaver.server.WebAppSessionManager; +import io.cloudbeaver.server.WebAppUtils; import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.security.exception.SMAccessTokenExpiredException; -import javax.servlet.http.HttpServletRequest; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.List; @@ -37,68 +28,23 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -public class CBJettyWebSocketManager implements JettyWebSocketCreator { +public class CBJettyWebSocketManager { private static final Log log = Log.getLog(CBJettyWebSocketManager.class); - private final Map> socketBySessionId = new ConcurrentHashMap<>(); - private final WebSessionManager webSessionManager; + private static final Map> socketBySessionId = new ConcurrentHashMap<>(); - public CBJettyWebSocketManager(@NotNull WebSessionManager webSessionManager) { - this.webSessionManager = webSessionManager; - - new WebSocketPingPongJob(CBPlatform.getInstance(), this).scheduleMonitor(); - } - - @Nullable - @Override - public Object createWebSocket(@NotNull JettyServerUpgradeRequest request, JettyServerUpgradeResponse resp) { - var httpRequest = request.getHttpServletRequest(); - var webSession = webSessionManager.getOrRestoreSession(httpRequest); - if (webSession != null) { - // web client session - return createNewEventsWebSocket(webSession); - } - // possible desktop client session - try { - var headlessSession = createHeadlessSession(httpRequest); - if (headlessSession == null) { - log.debug("Couldn't create headless session"); - return null; - } - return createNewEventsWebSocket(headlessSession); - } catch (SMAccessTokenExpiredException e) { - return new CBExpiredSessionWebSocket(); - } catch (DBException e) { - log.error("Error resolve websocket session", e); - return null; - } - } - - @NotNull - private CBEventsWebSocket createNewEventsWebSocket(@NotNull BaseWebSession webSession) { - var sessionId = webSession.getSessionId(); - var newWebSocket = new CBEventsWebSocket(webSession); - socketBySessionId.computeIfAbsent(sessionId, key -> new CopyOnWriteArrayList<>()) - .add(newWebSocket); - log.info("Websocket created for session: " + sessionId); - return newWebSocket; + private CBJettyWebSocketManager() { } - @Nullable - private WebHeadlessSession createHeadlessSession(@NotNull HttpServletRequest request) throws DBException { - var httpSession = request.getSession(false); - if (httpSession == null) { - log.debug("CloudBeaver web session not exist, try to create headless session"); - } else { - log.debug("CloudBeaver session not found with id " + httpSession.getId() + ", try to create headless session"); - } - return webSessionManager.getHeadlessSession(request, true); + public static void registerWebSocket(@NotNull String webSessionId, @NotNull CBEventsWebSocket webSocket) { + socketBySessionId.computeIfAbsent(webSessionId, key -> new CopyOnWriteArrayList<>()).add(webSocket); } - public void sendPing() { + public static void sendPing() { //remove expired sessions + WebAppSessionManager webSessionManager = WebAppUtils.getWebApplication().getSessionManager(); socketBySessionId.entrySet() .removeIf(entry -> { - entry.getValue().removeIf(ws -> !ws.isConnected()); + entry.getValue().removeIf(ws -> !ws.isOpen()); return webSessionManager.getSession(entry.getKey()) == null || entry.getValue().isEmpty(); } @@ -113,9 +59,8 @@ public void sendPing() { var webSockets = entry.getValue(); for (CBEventsWebSocket webSocket : webSockets) { try { - webSocket.getRemote().sendPing( - ByteBuffer.wrap("cb-ping".getBytes(StandardCharsets.UTF_8)), - webSocket.getCallback() + webSocket.getSession().getBasicRemote().sendPing( + ByteBuffer.wrap("cb-ping".getBytes(StandardCharsets.UTF_8)) ); } catch (Exception e) { log.error("Failed to send ping in web socket: " + sessionId); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBWebSocketServerConfigurator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBWebSocketServerConfigurator.java new file mode 100644 index 0000000000..1948ce5629 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBWebSocketServerConfigurator.java @@ -0,0 +1,123 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.websockets; + +import io.cloudbeaver.model.session.WebHeadlessSession; +import io.cloudbeaver.model.session.WebHttpRequestInfo; +import io.cloudbeaver.server.HttpConstants; +import io.cloudbeaver.server.WebAppSessionManager; +import jakarta.servlet.http.HttpSession; +import jakarta.websocket.HandshakeResponse; +import jakarta.websocket.server.HandshakeRequest; +import jakarta.websocket.server.ServerEndpointConfig; +import org.eclipse.jetty.ee10.websocket.jakarta.server.internal.JakartaWebSocketCreator; +import org.eclipse.jetty.http.BadMessageException; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.security.exception.SMAccessTokenExpiredException; +import org.jkiss.dbeaver.model.websocket.WSConstants; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.WSClientUtils; + +import java.util.List; + +public class CBWebSocketServerConfigurator extends ServerEndpointConfig.Configurator { + private static final Log log = Log.getLog(CBWebSocketServerConfigurator.class); + + public static final String PROP_WEB_SESSION = "cb-session"; + public static final String PROP_TOKEN_EXPIRED = "cb-token-expired"; + + @NotNull + private final WebAppSessionManager webSessionManager; + + public CBWebSocketServerConfigurator(@NotNull WebAppSessionManager sessionManager) { + this.webSessionManager = sessionManager; + } + + @Override + public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { + String sessionId = getSessionId(request); + + String userAgentHeader = request.getHeaders() + .get(WebHttpRequestInfo.USER_AGENT) + .stream() + .findFirst() + .orElse(null); + + WebHttpRequestInfo requestInfo = new WebHttpRequestInfo( + sessionId, + sec.getUserProperties().get(JakartaWebSocketCreator.PROP_LOCALES), + CommonUtils.toString(sec.getUserProperties().get(JakartaWebSocketCreator.PROP_REMOTE_ADDRESS)), + userAgentHeader + ); + + var webSession = webSessionManager.getOrRestoreWebSession(requestInfo); + + if (webSession != null) { + webSession.updateSessionParameters(requestInfo); + // web client session + sec.getUserProperties().put(PROP_WEB_SESSION, webSession); + } + // possible desktop client session + try { + var headlessSession = createHeadlessSession(requestInfo, request); + if (headlessSession != null) { + sec.getUserProperties().put(PROP_WEB_SESSION, headlessSession); + } else { + log.trace("Couldn't create headless session"); + } + } catch (SMAccessTokenExpiredException e) { + sec.getUserProperties().put(PROP_TOKEN_EXPIRED, true); + } catch (DBException e) { + log.error("Error resolve websocket session", e); + throw new RuntimeException(e.getMessage(), e); + } + if (sec.getUserProperties().get(PROP_WEB_SESSION) == null) { + throw new BadMessageException("No web session found for websocket request"); + } + } + + @Nullable + private String getSessionId(@NotNull HandshakeRequest request) { + // complex auth uses bearer authentication + List authHeaders = WSClientUtils.getHeaders(request.getHeaders(), HttpConstants.HEADER_AUTHORIZATION); + if (!CommonUtils.isEmpty(authHeaders) && authHeaders.get(0).startsWith("Bearer ")) { + return authHeaders.get(0).substring(7); + } + return request.getHttpSession() instanceof HttpSession httpSession ? httpSession.getId() : null; + } + + @Nullable + private WebHeadlessSession createHeadlessSession( + @NotNull WebHttpRequestInfo requestInfo, + @NotNull HandshakeRequest request + ) throws DBException { + if (request.getHeaders() == null) { + return null; + } + List tokenHeaders = WSClientUtils.getHeaders(request.getHeaders(), WSConstants.WS_AUTH_HEADER); + if (CommonUtils.isEmpty(tokenHeaders)) { + return null; + } + String smAccessToken = tokenHeaders.stream() + .findFirst() + .orElse(null); + return webSessionManager.getHeadlessSession(smAccessToken, requestInfo, true); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java index 8530e963c1..79e875f238 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2022 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -18,10 +18,11 @@ import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebHeadlessSession; -import org.eclipse.jetty.websocket.api.WriteCallback; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.PongMessage; import org.jkiss.code.NotNull; -public class WebSocketPingPongCallback implements WriteCallback { +public class WebSocketPingPongCallback implements MessageHandler.Whole { @NotNull private final BaseWebSession webSession; @@ -30,7 +31,7 @@ public WebSocketPingPongCallback(@NotNull BaseWebSession webSession) { } @Override - public void writeSuccess() { + public void onMessage(PongMessage message) { if (webSession instanceof WebHeadlessSession) { webSession.touchSession(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java index 009492f97b..1a47842e8e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,26 +16,26 @@ */ package io.cloudbeaver.server.websockets; -import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.server.BaseWebPlatform; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.jkiss.dbeaver.model.runtime.AbstractJob; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import java.time.Duration; + /** * WebSessionMonitorJob */ -class WebSocketPingPongJob extends AbstractJob { - private static final int INTERVAL = 1000 * 60 * 1; // once per 1 min - private final CBPlatform platform; - private final CBJettyWebSocketManager webSocketManager; +public class WebSocketPingPongJob extends AbstractJob { + private static final long INTERVAL = Duration.ofSeconds(30).toMillis(); // once per 1 min + private final BaseWebPlatform platform; - public WebSocketPingPongJob(CBPlatform platform, CBJettyWebSocketManager webSocketManager) { + public WebSocketPingPongJob(BaseWebPlatform platform) { super("WebSocket monitor"); this.platform = platform; setUser(false); setSystem(true); - this.webSocketManager = webSocketManager; } @Override @@ -44,7 +44,7 @@ protected IStatus run(DBRProgressMonitor monitor) { return Status.OK_STATUS; } - webSocketManager.sendPing(); + CBJettyWebSocketManager.sendPing(); if (!platform.isShuttingDown()) { scheduleMonitor(); @@ -52,7 +52,7 @@ protected IStatus run(DBRProgressMonitor monitor) { return Status.OK_STATUS; } - void scheduleMonitor() { + public void scheduleMonitor() { schedule(INTERVAL); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/ConnectionController.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/ConnectionController.java new file mode 100644 index 0000000000..5ba9d33d82 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/ConnectionController.java @@ -0,0 +1,99 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.WebObjectId; +import io.cloudbeaver.model.WebConnectionConfig; +import io.cloudbeaver.model.WebConnectionInfo; +import io.cloudbeaver.model.WebNetworkHandlerConfigInput; +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.registry.DataSourceDescriptor; + +import java.util.List; +import java.util.Map; + + +public interface ConnectionController { + + DBPDataSourceContainer createDataSourceContainer( + @NotNull WebSession webSession, + @Nullable @WebObjectId String projectId, + @NotNull WebConnectionConfig connectionConfig + ) throws DBWebException; + + WebConnectionInfo createConnection( + @NotNull WebSession webSession, + @Nullable String projectId, + DBPDataSourceRegistry sessionRegistry, + DBPDataSourceContainer newDataSource + ) throws DBWebException; + + DBPDataSourceContainer getDatasourceConnection( + @NotNull WebSession webSession, + @Nullable @WebObjectId String projectId, + @NotNull WebConnectionConfig connectionConfig) throws DBWebException; + + WebConnectionInfo updateConnection( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull WebConnectionConfig config, + DBPDataSourceContainer dataSource, + DBPDataSourceRegistry sessionRegistry + ) throws DBWebException; + + boolean deleteConnection( + @NotNull WebSession webSession, + @Nullable @WebObjectId String projectId, + @NotNull String connectionId) throws DBWebException; + + DataSourceDescriptor prepareTestConnection( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull WebConnectionConfig connectionConfig) throws DBWebException; + + WebConnectionInfo testConnection( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull WebConnectionConfig connectionConfig, + DataSourceDescriptor dataSource + ) throws DBWebException; + + WebConnectionInfo getConnectionState( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull String connectionId + ) throws DBWebException; + + WebConnectionInfo initConnection( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull String connectionId, + @NotNull Map authProperties, + @Nullable List networkCredentials, + boolean saveCredentials, + boolean sharedCredentials, + @Nullable String selectedSecretId + ) throws DBWebException; + + void validateConnection(DBPDataSourceContainer dataSourceContainer) throws DBWebException; + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/ConnectionControllerCE.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/ConnectionControllerCE.java new file mode 100644 index 0000000000..f99e311c1e --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/ConnectionControllerCE.java @@ -0,0 +1,496 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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; + +import io.cloudbeaver.*; +import io.cloudbeaver.model.WebConnectionConfig; +import io.cloudbeaver.model.WebConnectionInfo; +import io.cloudbeaver.model.WebNetworkHandlerConfigInput; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.utils.ServletAppUtils; +import io.cloudbeaver.utils.WebDataSourceUtils; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.exec.DBCConnectException; +import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration; +import org.jkiss.dbeaver.model.net.DBWHandlerType; +import org.jkiss.dbeaver.model.rm.RMProjectType; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.secret.DBSSecretValue; +import org.jkiss.dbeaver.registry.DataSourceDescriptor; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob; +import org.jkiss.dbeaver.utils.RuntimeUtils; +import org.jkiss.utils.CommonUtils; + +import java.util.List; +import java.util.Map; + +public class ConnectionControllerCE implements ConnectionController { + + private static final Log log = Log.getLog(ConnectionController.class); + + + @Override + public DBPDataSourceContainer createDataSourceContainer( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull WebConnectionConfig connectionConfig + ) throws DBWebException { + WebSessionProjectImpl project = getProjectById(webSession, projectId); + var rmProject = project.getRMProject(); + if (rmProject.getType() == RMProjectType.USER + && !webSession.hasPermission(DBWConstants.PERMISSION_ADMIN) + && !ServletAppUtils.getServletApplication().getAppConfiguration().isSupportsCustomConnections() + ) { + throw new DBWebException("New connection create is restricted by server configuration"); + } + webSession.addInfoMessage("Create new connection"); + DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); + + DBPDataSourceContainer newDataSource = WebServiceUtils.createConnectionFromConfig(connectionConfig, + sessionRegistry); + if (CommonUtils.isEmpty(newDataSource.getName())) { + newDataSource.setName(CommonUtils.notNull(connectionConfig.getName(), "NewConnection")); + } + return newDataSource; + } + + @Override + @NotNull + public WebConnectionInfo createConnection( + @NotNull WebSession webSession, + @Nullable String projectId, + DBPDataSourceRegistry sessionRegistry, + DBPDataSourceContainer newDataSource + ) throws DBWebException { + WebSessionProjectImpl project = getProjectById(webSession, projectId); + try { + sessionRegistry.addDataSource(newDataSource); + + sessionRegistry.checkForErrors(); + } catch (DBException e) { + sessionRegistry.removeDataSource(newDataSource); + throw new DBWebException("Failed to create connection", e); + } + + WebConnectionInfo connectionInfo = project.addConnection(newDataSource); + webSession.addInfoMessage("New connection was created - " + WebServiceUtils.getConnectionContainerInfo( + newDataSource)); + log.info(String.format( + "New connection was created: [info=%s, user=%s]", + WebServiceUtils.getConnectionContainerInfo(newDataSource), + webSession.getUserId() + )); + return connectionInfo; + } + + @Override + public DBPDataSourceContainer getDatasourceConnection( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull WebConnectionConfig config + ) throws DBWebException { + // Do not check for custom connection option. Already created connections can be edited. + // Also template connections can be edited +// if (!CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections()) { +// throw new DBWebException("Connection edit is restricted by server configuration"); +// } + + WebConnectionInfo connectionInfo = WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, config.getConnectionId()); + DBPDataSourceContainer dataSource = connectionInfo.getDataSourceContainer(); + webSession.addInfoMessage("Update connection - " + WebServiceUtils.getConnectionContainerInfo(dataSource)); + getOldDataSource(dataSource); + if (!CommonUtils.isEmpty(config.getName())) { + dataSource.setName(config.getName()); + } + + if (config.getDescription() != null) { + dataSource.setDescription(config.getDescription()); + } + + WebSessionProjectImpl project = getProjectById(webSession, projectId); + DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); + dataSource.setFolder(config.getFolder() != null ? sessionRegistry.getFolder(config.getFolder()) : null); + if (config.isDefaultAutoCommit() != null) { + dataSource.setDefaultAutoCommit(config.isDefaultAutoCommit()); + } + dataSource.setConnectionReadOnly(config.isReadOnly()); + WebServiceUtils.setConnectionConfiguration(dataSource.getDriver(), + dataSource.getConnectionConfiguration(), + config); + + // we should check that the config has changed but not check for password changes + dataSource.setSharedCredentials(config.isSharedCredentials()); + dataSource.setSavePassword(config.isSaveCredentials()); + boolean sharedCredentials = isSharedCredentials(dataSource); + if (sharedCredentials) { + //we must notify about the shared password change + WebServiceUtils.saveAuthProperties( + dataSource, + dataSource.getConnectionConfiguration(), + config.getCredentials(), + config.isSaveCredentials(), + config.isSharedCredentials() + ); + } + connectionInfo.setCredentialsSavedInSession(null); + // same here + return dataSource; + } + + private static boolean isSharedCredentials(DBPDataSourceContainer dataSource) { + return dataSource.isSharedCredentials() || !dataSource.getProject() + .isUseSecretStorage() && dataSource.isSavePassword(); + } + + private static DataSourceDescriptor getOldDataSource(DBPDataSourceContainer dataSource) { + DataSourceDescriptor oldDataSource; + oldDataSource = dataSource.getRegistry().createDataSource(dataSource); + oldDataSource.setId(dataSource.getId()); + return oldDataSource; + } + + @Override + public WebConnectionInfo updateConnection( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull WebConnectionConfig config, + DBPDataSourceContainer dataSource, + DBPDataSourceRegistry sessionRegistry + ) throws DBWebException { + WebConnectionInfo connectionInfo = WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, config.getConnectionId()); + if (!isSharedCredentials(dataSource)) { + // secret controller is responsible for notification, password changes applied after checks + WebServiceUtils.saveAuthProperties( + dataSource, + dataSource.getConnectionConfiguration(), + config.getCredentials(), + config.isSaveCredentials(), + config.isSharedCredentials() + ); + } + try { + sessionRegistry.updateDataSource(dataSource); + sessionRegistry.checkForErrors(); + } catch (DBException e) { + throw new DBWebException("Failed to update connection", e); + } + log.info(String.format( + "Connection updated: [info=%s, userId=%s]", + WebServiceUtils.getConnectionContainerInfo(dataSource), + webSession.getUser() + )); + return connectionInfo; + } + + @Override + public boolean deleteConnection( + @NotNull WebSession webSession, @Nullable String projectId, @NotNull String connectionId + ) throws DBWebException { + WebConnectionInfo connectionInfo = WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, connectionId); + if (connectionInfo.getDataSourceContainer().getProject() != getProjectById(webSession, projectId)) { + throw new DBWebException("Global connection '" + connectionInfo.getName() + "' configuration cannot be deleted"); + } + webSession.addInfoMessage("Delete connection - " + + WebServiceUtils.getConnectionContainerInfo(connectionInfo.getDataSourceContainer())); + closeAndDeleteConnection(webSession, projectId, connectionId, true); + + log.info(String.format( + "Connection deleted: [info=%s, userId=%s]", + WebServiceUtils.getConnectionContainerInfo(connectionInfo.getDataSourceContainer()), + webSession.getUserId() + )); + return true; + } + + @Override + public DataSourceDescriptor prepareTestConnection( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull WebConnectionConfig connectionConfig + ) throws DBWebException { + String connectionId = connectionConfig.getConnectionId(); + + connectionConfig.setSaveCredentials(true); // It is used in createConnectionFromConfig + + DataSourceDescriptor dataSource = (DataSourceDescriptor) WebDataSourceUtils.getLocalOrGlobalDataSource( + webSession, projectId, connectionId); + + validateConnection(dataSource); + + WebProjectImpl project = getProjectById(webSession, projectId); + DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); + DataSourceDescriptor testDataSource; + if (dataSource != null) { + try { + // Check that creds are saved to trigger secrets resolve + dataSource.isCredentialsSaved(); + } catch (DBException e) { + throw new DBWebException("Can't determine whether datasource credentials are saved", e); + } + + testDataSource = (DataSourceDescriptor) dataSource.createCopy(dataSource.getRegistry()); + WebServiceUtils.setConnectionConfiguration( + testDataSource.getDriver(), + testDataSource.getConnectionConfiguration(), + connectionConfig + ); + if (connectionConfig.getSelectedSecretId() != null) { + try { + dataSource.listSharedCredentials() + .stream() + .filter(secret -> connectionConfig.getSelectedSecretId().equals(secret.getSubjectId())) + .findFirst() + .ifPresent(testDataSource::setSelectedSharedCredentials); + + } catch (DBException e) { + throw new DBWebException("Failed to load secret value: " + connectionConfig.getSelectedSecretId()); + } + } + WebServiceUtils.saveAuthProperties( + testDataSource, + testDataSource.getConnectionConfiguration(), + connectionConfig.getCredentials(), + true, + false, + true + ); + } else { + testDataSource = (DataSourceDescriptor) WebServiceUtils.createConnectionFromConfig(connectionConfig, + sessionRegistry); + } + return testDataSource; + } + + @Override + @NotNull + public WebConnectionInfo testConnection( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull WebConnectionConfig connectionConfig, + DataSourceDescriptor testDataSource + ) throws DBWebException { + validateDriverLibrariesPresence(testDataSource); + webSession.provideAuthParameters(webSession.getProgressMonitor(), + testDataSource, + testDataSource.getConnectionConfiguration()); + testDataSource.setSavePassword(true); // We need for test to avoid password callback + if (DataSourceDescriptor.class.isAssignableFrom(testDataSource.getClass())) { + testDataSource.setAccessCheckRequired(!webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)); + } + try { + ConnectionTestJob ct = new ConnectionTestJob(testDataSource, param -> { + }); + ct.run(webSession.getProgressMonitor()); + if (ct.getConnectError() != null) { + if (ct.getConnectError() instanceof DBCConnectException error) { + Throwable rootCause = CommonUtils.getRootCause(error); + if (rootCause instanceof ClassNotFoundException) { + throwDriverNotFoundException(testDataSource); + } + } + throw new DBWebException("Connection failed", ct.getConnectError()); + } + WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, testDataSource); + connectionInfo.setConnectError(ct.getConnectError()); + connectionInfo.setServerVersion(ct.getServerVersion()); + connectionInfo.setClientVersion(ct.getClientVersion()); + connectionInfo.setConnectTime(RuntimeUtils.formatExecutionTime(ct.getConnectTime())); + return connectionInfo; + } catch (DBException e) { + throw new DBWebException("Error connecting to database", e); + } + } + + private WebSessionProjectImpl getProjectById(WebSession webSession, String projectId) throws DBWebException { + WebSessionProjectImpl project = webSession.getProjectById(projectId); + if (project == null) { + throw new DBWebException("Project '" + projectId + "' not found"); + } + return project; + } + + @NotNull + private WebConnectionInfo closeAndDeleteConnection( + @NotNull WebSession webSession, + @NotNull String projectId, + @NotNull String connectionId, + boolean forceDelete + ) throws DBWebException { + WebSessionProjectImpl project = getProjectById(webSession, projectId); + WebConnectionInfo connectionInfo = project.getWebConnectionInfo(connectionId); + + DBPDataSourceContainer dataSourceContainer = connectionInfo.getDataSourceContainer(); + boolean disconnected = WebDataSourceUtils.disconnectDataSource(webSession, dataSourceContainer); + if (forceDelete) { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + registry.removeDataSource(dataSourceContainer); + try { + registry.checkForErrors(); + } catch (DBException e) { + try { + registry.addDataSource(dataSourceContainer); + } catch (DBException ex) { + log.error("Error re-adding after delete attempt", e); + } + throw new DBWebException("Failed to delete connection", e); + } + project.removeConnection(dataSourceContainer); + } else { + // Just reset saved credentials + connectionInfo.clearCache(); + } + + return connectionInfo; + } + + @Override + public WebConnectionInfo getConnectionState( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull String connectionId + ) throws DBWebException { + return WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, connectionId); + } + + @Override + public WebConnectionInfo initConnection(@NotNull WebSession webSession, @Nullable String projectId, @NotNull String connectionId, @NotNull Map authProperties, @Nullable List networkCredentials, boolean saveCredentials, boolean sharedCredentials, @Nullable String selectedSecretId) throws DBWebException { + WebConnectionInfo connectionInfo = WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, connectionId); + connectionInfo.setSavedCredentials(authProperties, networkCredentials); + + var dataSourceContainer = connectionInfo.getDataSourceContainer();; + validateConnection(dataSourceContainer); + if (dataSourceContainer.isConnected()) { + throw new DBWebException("Datasource '" + dataSourceContainer.getName() + "' is already connected"); + } + if (dataSourceContainer.isSharedCredentials() && selectedSecretId != null) { + List allSecrets; + try { + allSecrets = dataSourceContainer.listSharedCredentials(); + } catch (DBException e) { + throw new DBWebException("Error loading connection secret", e); + } + DBSSecretValue selectedSecret = + allSecrets.stream() + .filter(secret -> selectedSecretId.equals(secret.getUniqueId())) + .findFirst().orElse(null); + if (selectedSecret == null) { + throw new DBWebException("Secret not found:" + selectedSecretId); + } + dataSourceContainer.setSelectedSharedCredentials(selectedSecret); + } + + boolean oldSavePassword = dataSourceContainer.isSavePassword(); + DBRProgressMonitor monitor = webSession.getProgressMonitor(); + validateDriverLibrariesPresence(dataSourceContainer); + try { + boolean connect = dataSourceContainer.connect(monitor, true, false); + } catch (Exception e) { + if (e instanceof DBCConnectException) { + Throwable rootCause = CommonUtils.getRootCause(e); + if (rootCause instanceof ClassNotFoundException) { + throwDriverNotFoundException(dataSourceContainer); + } + } + throw new DBWebException("Error connecting to database", e); + } finally { + dataSourceContainer.setSavePassword(oldSavePassword); + connectionInfo.clearCache(); + } + // Mark all specified network configs as saved + boolean[] saveConfig = new boolean[1]; + + if (networkCredentials != null) { + networkCredentials.forEach(c -> { + if (CommonUtils.toBoolean(c.isSavePassword())) { + DBWHandlerConfiguration handlerCfg = dataSourceContainer.getConnectionConfiguration() + .getHandler(c.getId()); + if (handlerCfg != null && + // check username param only for ssh config + !(CommonUtils.isEmpty(c.getUserName()) && CommonUtils.equalObjects(handlerCfg.getType(), + DBWHandlerType.TUNNEL)) + ) { + WebDataSourceUtils.updateHandlerCredentials(handlerCfg, c); + handlerCfg.setSavePassword(true); + saveConfig[0] = true; + } + } + }); + } + if (saveCredentials) { + // Save all passed credentials in the datasource container + WebServiceUtils.saveAuthProperties( + dataSourceContainer, + dataSourceContainer.getConnectionConfiguration(), + authProperties, + true, + sharedCredentials + ); + + var project = dataSourceContainer.getProject(); + if (project.isUseSecretStorage()) { + try { + dataSourceContainer.persistSecrets( + DBSSecretController.getProjectSecretController(dataSourceContainer.getProject()) + ); + } catch (DBException e) { + throw new DBWebException("Failed to save credentials", e); + } + } + + WebDataSourceUtils.saveCredentialsInDataSource(connectionInfo, + dataSourceContainer, + dataSourceContainer.getConnectionConfiguration()); + saveConfig[0] = true; + } + if (WebServiceUtils.isGlobalProject(dataSourceContainer.getProject())) { + // Do not flush config for global project (only admin can do it - CB-2415) + if (saveCredentials) { + connectionInfo.setCredentialsSavedInSession(true); + } + saveConfig[0] = false; + } + if (saveConfig[0]) { + dataSourceContainer.persistConfiguration(); + } + + return connectionInfo; + } + + @Override + public void validateConnection(DBPDataSourceContainer dataSourceContainer) throws DBWebException { + } + + + private void validateDriverLibrariesPresence(@NotNull DBPDataSourceContainer container) throws DBWebException { + if (!DBWorkbench.isDistributed() && container.getDriver().getDriverLoader(container).needsExternalDependencies()) { + throwDriverNotFoundException(container); + } + } + + @NotNull + private static String throwDriverNotFoundException(@NotNull DBPDataSourceContainer container) throws DBWebException { + throw new DBWebException("Driver files for %s are not found. Please ask the administrator to download it." + .formatted(container.getDriver().getName())); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWBindingContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWBindingContext.java index 7a7d84e935..29507e27e6 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWBindingContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWBindingContext.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceBindingGraphQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceBindingGraphQL.java index 851405c00a..a5cbcbf9f0 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceBindingGraphQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceBindingGraphQL.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceBindingWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceBindingWebSocket.java new file mode 100644 index 0000000000..47c2718825 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceBindingWebSocket.java @@ -0,0 +1,29 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import io.cloudbeaver.model.app.ServletApplication; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; + +public interface DBWServiceBindingWebSocket extends DBWServiceBinding { + default boolean isApplicable(@NotNull ServletApplication application) { + return true; + } + + void addWebSockets(@NotNull APPLICATION application, @NotNull DBWWebSocketContext context) throws DBException; +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServletHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServletHandler.java index 6a0a6da154..edf17e59ff 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServletHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServletHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,11 +16,11 @@ */ package io.cloudbeaver.service; +import jakarta.servlet.Servlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jkiss.dbeaver.DBException; -import javax.servlet.Servlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWWebSocketContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWWebSocketContext.java new file mode 100644 index 0000000000..8b9a050ea5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWWebSocketContext.java @@ -0,0 +1,25 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; + +import jakarta.websocket.server.ServerEndpointConfig; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; + +public interface DBWWebSocketContext { + void addWebSocket(@NotNull ServerEndpointConfig endpointConfig) throws DBException; +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java index 220389e12f..7f0fe16067 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -21,12 +21,16 @@ import graphql.schema.idl.TypeDefinitionRegistry; import io.cloudbeaver.*; import io.cloudbeaver.model.WebConnectionInfo; +import io.cloudbeaver.model.app.ServletApplication; +import io.cloudbeaver.model.cli.CloudbeaverCliConstants; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebSessionProvider; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.server.WebAppUtils; import io.cloudbeaver.server.graphql.GraphQLEndpoint; import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.utils.ServletAppUtils; +import io.cloudbeaver.utils.WebDataSourceUtils; +import jakarta.servlet.http.HttpServletResponse; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -39,8 +43,6 @@ import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.*; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; /** * Web service implementation @@ -64,19 +66,24 @@ protected API_TYPE getServiceImpl() { } @Override - public TypeDefinitionRegistry getTypeDefinition() throws DBWebException { + @Nullable + public TypeDefinitionRegistry getTypeDefinition() { return loadSchemaDefinition(getClass(), schemaFileName); } /** * Creates proxy for permission checks and other general API calls validation/logging. */ - protected API_TYPE getService(DataFetchingEnvironment env) { + protected API_TYPE getService(DataFetchingEnvironment env) { Object proxyImpl = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{apiInterface}, new ServiceInvocationHandler(serviceImpl, env)); return apiInterface.cast(proxyImpl); } - public static TypeDefinitionRegistry loadSchemaDefinition(Class theClass, String schemaPath) throws DBWebException { + @Nullable + public static TypeDefinitionRegistry loadSchemaDefinition(@NotNull Class theClass, @Nullable String schemaPath) { + if (schemaPath == null) { + return null; + } try (InputStream schemaStream = theClass.getClassLoader().getResourceAsStream(schemaPath)) { if (schemaStream == null) { throw new IOException("Schema file '" + schemaPath + "' not found"); @@ -89,10 +96,6 @@ public static TypeDefinitionRegistry loadSchemaDefinition(Class theClass, Str } } - protected static HttpServletRequest getServletRequest(DataFetchingEnvironment env) { - return GraphQLEndpoint.getServletRequest(env); - } - protected static HttpServletResponse getServletResponse(DataFetchingEnvironment env) { return GraphQLEndpoint.getServletResponse(env); } @@ -102,13 +105,34 @@ protected static DBWBindingContext getBindingContext(DataFetchingEnvironment env } protected static WebSession getWebSession(DataFetchingEnvironment env) throws DBWebException { - return CBPlatform.getInstance().getSessionManager().getWebSession( - getServletRequest(env), getServletResponse(env)); + if (env.getGraphQlContext().getBoolean(CloudbeaverCliConstants.CLI_MODE)) { + return getSessionFromContextOrThrow(env); + } + return WebAppUtils.getWebApplication().getSessionManager().getWebSession( + GraphQLEndpoint.getServletRequestOrThrow(env), getServletResponse(env)); + } + + @Nullable + protected static WebSession getSessionFromContext(DataFetchingEnvironment env) { + WebSession webSession = env.getGraphQlContext().get(WebSession.class.getName()); + return webSession; + } + + @NotNull + protected static WebSession getSessionFromContextOrThrow(DataFetchingEnvironment env) throws DBWebException { + WebSession webSession = env.getGraphQlContext().get(WebSession.class.getName()); + if (webSession == null) { + throw new DBWebException("Web session not found in GraphQL context"); + } + return webSession; } protected static WebSession getWebSession(DataFetchingEnvironment env, boolean errorOnNotFound) throws DBWebException { - return CBPlatform.getInstance().getSessionManager().getWebSession( - getServletRequest(env), getServletResponse(env), errorOnNotFound); + if (env.getGraphQlContext().getBoolean(CloudbeaverCliConstants.CLI_MODE)) { + return getSessionFromContextOrThrow(env); + } + return WebAppUtils.getWebApplication().getSessionManager().getWebSession( + GraphQLEndpoint.getServletRequestOrThrow(env), getServletResponse(env), errorOnNotFound); } protected static String getProjectReference(DataFetchingEnvironment env) { @@ -125,18 +149,21 @@ protected static WebConnectionInfo getWebConnection(DataFetchingEnvironment env) */ @Nullable public static WebSession findWebSession(DataFetchingEnvironment env) { - return CBPlatform.getInstance().getSessionManager().findWebSession( - getServletRequest(env)); + if (env.getGraphQlContext().getBoolean(CloudbeaverCliConstants.CLI_MODE)) { + return getSessionFromContext(env); + } + return WebAppUtils.getWebApplication().getSessionManager().findWebSession( + GraphQLEndpoint.getServletRequestOrThrow(env)); } public static WebSession findWebSession(DataFetchingEnvironment env, boolean errorOnNotFound) throws DBWebException { - return CBPlatform.getInstance().getSessionManager().findWebSession( - getServletRequest(env), errorOnNotFound); + return WebAppUtils.getWebApplication().getSessionManager().findWebSession( + GraphQLEndpoint.getServletRequestOrThrow(env), errorOnNotFound); } @NotNull public static WebConnectionInfo getWebConnection(WebSession session, String projectId, String connectionId) throws DBWebException { - return session.getWebConnectionInfo(projectId, connectionId); + return WebDataSourceUtils.getWebConnectionInfo(session, projectId, connectionId); } private class ServiceInvocationHandler implements InvocationHandler { @@ -161,7 +188,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl checkActionPermissions(method, webAction); } WebProjectAction projectAction = method.getAnnotation(WebProjectAction.class); - if(projectAction != null) { + if (projectAction != null) { checkObjectActionPermissions(method, projectAction, args); } beforeWebActionCall(webAction, method, args); @@ -175,7 +202,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } } catch (Throwable ex) { log.error("Unexpected error during gql request", ex); - if (SMUtils.isTokenExpiredExceptionWasHandled(ex)) { + if (SMUtils.isRefreshTokenExpiredExceptionWasHandled(ex)) { WebSession webSession = findWebSession(env); if (webSession != null) { webSession.resetUserState(); @@ -198,7 +225,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl private void checkObjectActionPermissions(Method method, WebProjectAction objectAction, Object[] args) throws DBException { WebSession webSession = findWebSession(env); - if (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) { + if (webSession != null && webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) { return; } String[] requireProjectPermissions = objectAction.requireProjectPermissions(); @@ -218,13 +245,18 @@ private void checkObjectActionPermissions(Method method, WebProjectAction object if (objectIdArgumentIndex < 0) { throw new DBWebExceptionAccessDenied("Project id argument not found"); } + if (webSession == null) { + throw new DBException("Web session not instantiated"); + } String projectId = args[objectIdArgumentIndex] == null ? null : String.valueOf(args[objectIdArgumentIndex]); + // we should always get the project from the session, even if projectId is null - the active project + // will be returned WebProjectImpl project = webSession.getProjectById(projectId); - if(project == null) { + if (project == null) { throw new DBException("Project not found:" + projectId); } - RMProject rmProject = project.getRmProject(); + RMProject rmProject = project.getRMProject(); for (String reqProjectPermission : requireProjectPermissions) { if (!rmProject.hasProjectPermission(reqProjectPermission)) { @@ -236,26 +268,31 @@ private void checkObjectActionPermissions(Method method, WebProjectAction object private void checkServicePermissions(Method method, WebActionSet actionSet) throws DBWebException { String[] features = actionSet.requireFeatures(); - if (features.length > 0) { - for (String feature : features) { - if (!CBApplication.getInstance().isConfigurationMode() && - !CBApplication.getInstance().getAppConfiguration().isFeatureEnabled(feature)) { - throw new DBWebException("Feature " + feature + " is disabled"); - } + ServletApplication servletApplication = ServletAppUtils.getServletApplication(); + for (String feature : features) { + if (!servletApplication.isConfigurationMode() && + !servletApplication.getAppConfiguration().isFeatureEnabled(feature)) { + throw new DBWebException("Feature " + feature + " is disabled"); } } } private void checkActionPermissions(@NotNull Method method, @NotNull WebAction webAction) throws DBWebException { + var application = WebAppUtils.getWebPlatform().getApplication(); + if (application.isInitializationMode() && webAction.initializationRequired()) { + String message = "Server initialization in progress: " + + String.join(",", application.getInitActions().values()) + ".\nDo not restart the server."; + throw new DBWebExceptionServerNotInitialized(message); + } String[] reqPermissions = webAction.requirePermissions(); - if (reqPermissions.length == 0 && !webAction.authRequired()) { + String[] reqGlobalPermissions = webAction.requireGlobalPermissions(); + if (reqPermissions.length == 0 && reqGlobalPermissions.length == 0 && !webAction.authRequired()) { return; } WebSession session = findWebSession(env); if (session == null) { throw new DBWebExceptionAccessDenied("No open session - anonymous access restricted"); } - CBApplication application = CBApplication.getInstance(); if (!application.isConfigurationMode()) { if (webAction.authRequired() && !session.isAuthorizedInSecurityManager()) { log.debug("Anonymous access to " + method.getName() + " restricted"); @@ -265,8 +302,13 @@ private void checkActionPermissions(@NotNull Method method, @NotNull WebAction w // Check license if (application.isLicenseRequired() && !application.isLicenseValid()) { if (!ArrayUtils.contains(reqPermissions, DBWConstants.PERMISSION_ADMIN)) { + String errorMessage = "Invalid server license"; + String licenseStatus = application.getLicenseStatus(); + if (licenseStatus != null) { + errorMessage = errorMessage + ": " + licenseStatus; + } // Only admin permissions are allowed - throw new DBWebExceptionLicenseRequired("Invalid server license"); + throw new DBWebExceptionLicenseRequired(errorMessage); } } // Check permissions @@ -276,6 +318,13 @@ private void checkActionPermissions(@NotNull Method method, @NotNull WebAction w throw new DBWebExceptionAccessDenied("Access denied"); } } + // Check permissions + for (String gp : reqGlobalPermissions) { + if (!session.hasGlobalPermission(gp)) { + log.debug("Access to " + method.getName() + " denied for " + session.getUser()); + throw new DBWebExceptionAccessDenied("Access denied"); + } + } } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java index 58f68f9f4a..a7709f7db2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java @@ -1,46 +1,89 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.cloudbeaver.model.apilog.ApiCallInterceptor; +import io.cloudbeaver.model.app.ServletApplication; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.server.WebAppUtils; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.data.json.JSONUtils; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.util.Map; public abstract class WebServiceServletBase extends HttpServlet { private static final Log log = Log.getLog(WebServiceServletBase.class); + private static final Type MAP_STRING_OBJECT_TYPE = JSONUtils.MAP_TYPE_TOKEN; + private static final String REQUEST_PARAM_VARIABLES = "variables"; + private static final Gson gson = new GsonBuilder() + .serializeNulls() + .setPrettyPrinting() + .create(); + public static final String API_PROTOCOL = "REST"; - private final CBApplication application; + private final ServletApplication application; - public WebServiceServletBase(CBApplication application) { + public WebServiceServletBase(ServletApplication application) { this.application = application; } - public CBApplication getApplication() { + public ServletApplication getApplication() { return application; } @Override protected final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - WebSession webSession = CBPlatform.getInstance().getSessionManager().findWebSession(request); + WebSession webSession = WebAppUtils.getWebApplication().getSessionManager().findWebSession(request); if (webSession == null) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Web session not found"); return; } + + LocalDateTime startTime = LocalDateTime.now(); + String errorMessage = null; try { processServiceRequest(webSession, request, response); } catch (Exception e) { log.error(e); - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error processing request: " + e.getMessage()); + errorMessage = e.getMessage(); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error processing request: " + errorMessage); + } finally { + if (WebAppUtils.getWebApplication() instanceof ApiCallInterceptor apiCallInterceptor) { + apiCallInterceptor.onApiCallEvent( + request, getVariables(request), request.getRequestURI(), null, startTime, errorMessage, API_PROTOCOL + ); + } } } protected abstract void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException; + protected Map getVariables(HttpServletRequest request) { + return gson.fromJson(request.getParameter(REQUEST_PARAM_VARIABLES), MAP_STRING_OBJECT_TYPE); + } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java index 10a116b558..1c8a48a4e5 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,20 +16,21 @@ */ package io.cloudbeaver.service.core; -import io.cloudbeaver.*; +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.WebAction; +import io.cloudbeaver.WebObjectId; +import io.cloudbeaver.WebProjectAction; import io.cloudbeaver.model.*; import io.cloudbeaver.model.session.WebSession; - import io.cloudbeaver.model.user.WebDataSourceProviderInfo; import io.cloudbeaver.service.DBWService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.model.rm.RMConstants; +import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.Part; -import java.util.Collection; import java.util.List; import java.util.Map; @@ -38,8 +39,17 @@ */ public interface DBWServiceCore extends DBWService { - @WebAction(authRequired = false) - WebServerConfig getServerConfig() throws DBWebException; + @WebAction(authRequired = false, initializationRequired = false) + WebServerConfig getServerConfig(@Nullable WebSession webSession) throws DBWebException; + + /** + * Returns information of system. + */ + @WebAction + WebPropertyInfo[] getSystemInformationProperties(@NotNull WebSession webSession); + + @WebAction + WebGroupPropertiesInfo getProductSettings(@NotNull WebSession webSession); @WebAction List getDriverList(@NotNull WebSession webSession, String driverId) throws DBWebException; @@ -61,13 +71,6 @@ List getUserConnections( List getConnectionFolders( @NotNull WebSession webSession, @Nullable String projectId, @Nullable String id) throws DBWebException; - @Deprecated - @WebAction - List getTemplateDataSources() throws DBWebException; - - @WebAction - List getTemplateConnections(@NotNull WebSession webSession, @Nullable String projectId) throws DBWebException; - @WebAction(authRequired = false) String[] getSessionPermissions(@NotNull WebSession webSession) throws DBWebException; @@ -90,9 +93,15 @@ WebSession openSession( @WebAction(authRequired = false) boolean closeSession(HttpServletRequest request) throws DBWebException; + @Deprecated @WebAction(authRequired = false) boolean touchSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse servletResponse) throws DBWebException; + @Deprecated + @WebAction(authRequired = false) + WebSession updateSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) + throws DBWebException; + @WebAction(authRequired = false) boolean refreshSessionConnections(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws DBWebException; @@ -112,8 +121,9 @@ WebConnectionInfo initConnection( @NotNull String connectionId, @NotNull Map authProperties, @Nullable List networkCredentials, - @Nullable Boolean saveCredentials, - @Nullable Boolean sharedCredentials + boolean saveCredentials, + boolean sharedCredentials, + @Nullable String selectedCredentials ) throws DBWebException; @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) @@ -135,13 +145,6 @@ boolean deleteConnection( @Nullable @WebObjectId String projectId, @NotNull String connectionId) throws DBWebException; - @WebAction - WebConnectionInfo createConnectionFromTemplate( - @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull String templateId, - @Nullable String connectionName) throws DBWebException; - @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) WebConnectionInfo copyConnectionFromNode( @NotNull WebSession webSession, @@ -199,10 +202,10 @@ WebConnectionInfo setConnectionNavigatorSettings( /////////////////////////////////////////// // Async tasks - @WebAction + @WebAction(authRequired = false) WebAsyncTaskInfo getAsyncTaskInfo(WebSession webSession, String taskId, Boolean removeOnFinish) throws DBWebException; - @WebAction + @WebAction(authRequired = false) boolean cancelAsyncTask(WebSession webSession, String taskId) throws DBWebException; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java index 1347234a8a..35891089f0 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -24,15 +24,16 @@ import io.cloudbeaver.model.WebConnectionConfig; import io.cloudbeaver.model.WebNetworkHandlerConfigInput; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.server.WebAppSessionManager; +import io.cloudbeaver.server.WebAppUtils; import io.cloudbeaver.server.graphql.GraphQLEndpoint; import io.cloudbeaver.service.DBWBindingContext; import io.cloudbeaver.service.WebServiceBindingBase; import io.cloudbeaver.service.core.impl.WebServiceCore; -import io.cloudbeaver.service.session.WebSessionManager; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jkiss.utils.CommonUtils; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.util.Collections; import java.util.List; import java.util.Map; @@ -49,19 +50,17 @@ public WebServiceBindingCore() { @Override public void bindWiring(DBWBindingContext model) throws DBWebException { - CBPlatform platform = CBPlatform.getInstance(); - WebSessionManager sessionManager = platform.getSessionManager(); + WebAppSessionManager sessionManager = WebAppUtils.getWebApplication().getSessionManager(); model.getQueryType() - .dataFetcher("serverConfig", env -> getService(env).getServerConfig()) + .dataFetcher("serverConfig", env -> getService(env).getServerConfig(findWebSession(env))) + .dataFetcher("systemInfo", env -> getService(env).getSystemInformationProperties(getWebSession(env))) + .dataFetcher("productSettings", env -> getService(env).getProductSettings(getWebSession(env))) .dataFetcher("driverList", env -> getService(env).getDriverList(getWebSession(env), env.getArgument("id"))) .dataFetcher("authModels", env -> getService(env).getAuthModels(getWebSession(env))) .dataFetcher("networkHandlers", env -> getService(env).getNetworkHandlers(getWebSession(env))) - .dataFetcher("templateDataSources", env -> getService(env).getTemplateDataSources()) .dataFetcher("userConnections", env -> getService(env).getUserConnections( getWebSession(env), getProjectReference(env), env.getArgument("id"), env.getArgument("projectIds"))) - .dataFetcher("templateConnections", env -> getService(env).getTemplateConnections( - getWebSession(env), getProjectReference(env))) .dataFetcher("connectionFolders", env -> getService(env).getConnectionFolders( getWebSession(env), getProjectReference(env), env.getArgument("path"))) @@ -88,7 +87,7 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { model.getMutationType() .dataFetcher("openSession", env -> { - HttpServletRequest servletRequest = GraphQLEndpoint.getServletRequest(env); + HttpServletRequest servletRequest = GraphQLEndpoint.getServletRequestOrThrow(env); HttpServletResponse servletResponse = GraphQLEndpoint.getServletResponse(env); return getService(env).openSession( sessionManager.getWebSession(servletRequest, servletResponse, false), @@ -96,11 +95,13 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { servletRequest, servletResponse); }) - .dataFetcher("closeSession", env -> getService(env).closeSession(GraphQLEndpoint.getServletRequest(env))) + .dataFetcher("closeSession", env -> getService(env).closeSession(GraphQLEndpoint.getServletRequestOrThrow(env))) .dataFetcher("touchSession", env -> getService(env).touchSession( - GraphQLEndpoint.getServletRequest(env), GraphQLEndpoint.getServletResponse(env))) + GraphQLEndpoint.getServletRequestOrThrow(env), GraphQLEndpoint.getServletResponse(env))) + .dataFetcher("updateSession", env -> getService(env).updateSession( + GraphQLEndpoint.getServletRequestOrThrow(env), GraphQLEndpoint.getServletResponse(env))) .dataFetcher("refreshSessionConnections", env -> getService(env).refreshSessionConnections( - GraphQLEndpoint.getServletRequest(env), GraphQLEndpoint.getServletResponse(env))) + GraphQLEndpoint.getServletRequestOrThrow(env), GraphQLEndpoint.getServletResponse(env))) .dataFetcher("changeSessionLanguage", env -> getService(env).changeSessionLanguage(getWebSession(env), env.getArgument("locale"))) .dataFetcher("createConnection", env -> getService(env).createConnection( @@ -109,11 +110,6 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getWebSession(env), getProjectReference(env), getConnectionConfig(env))) .dataFetcher("deleteConnection", env -> getService(env).deleteConnection( getWebSession(env), getProjectReference(env), env.getArgument("id"))) - .dataFetcher("createConnectionFromTemplate", env -> getService(env).createConnectionFromTemplate( - getWebSession(env), - getProjectReference(env), - env.getArgument("templateId"), - env.getArgument("connectionName"))) .dataFetcher("copyConnectionFromNode", env -> getService(env).copyConnectionFromNode( getWebSession(env), getProjectReference(env), @@ -131,8 +127,9 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env.getArgument("id"), env.getArgument("credentials"), nhc, - env.getArgument("saveCredentials"), - env.getArgument("sharedCredentials") + CommonUtils.toBoolean(env.getArgument("saveCredentials")), + CommonUtils.toBoolean(env.getArgument("sharedCredentials")), + env.getArgument("selectedSecretId") ); } ) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index 06b6c704b1..e094affe4e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -17,21 +17,22 @@ package io.cloudbeaver.service.core.impl; -import io.cloudbeaver.DBWConstants; -import io.cloudbeaver.DBWebException; -import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.*; import io.cloudbeaver.model.*; +import io.cloudbeaver.model.app.ServletApplication; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebHandlerRegistry; import io.cloudbeaver.registry.WebSessionHandlerDescriptor; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.server.WebAppUtils; +import io.cloudbeaver.server.WebApplication; import io.cloudbeaver.service.core.DBWServiceCore; import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.utils.ServletAppUtils; +import io.cloudbeaver.utils.WebCommonUtils; import io.cloudbeaver.utils.WebConnectionFolderUtils; import io.cloudbeaver.utils.WebDataSourceUtils; -import io.cloudbeaver.utils.WebEventUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -43,29 +44,26 @@ import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; import org.jkiss.dbeaver.model.connection.DBPDriver; -import org.jkiss.dbeaver.model.navigator.*; +import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; +import org.jkiss.dbeaver.model.navigator.DBNDataSource; +import org.jkiss.dbeaver.model.navigator.DBNModel; +import org.jkiss.dbeaver.model.navigator.DBNNode; import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration; import org.jkiss.dbeaver.model.net.DBWNetworkHandler; import org.jkiss.dbeaver.model.net.DBWTunnel; -import org.jkiss.dbeaver.model.net.ssh.SSHImplementation; +import org.jkiss.dbeaver.model.net.ssh.SSHSession; import org.jkiss.dbeaver.model.rm.RMProjectType; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; import org.jkiss.dbeaver.registry.DataSourceDescriptor; import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor; import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry; -import org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob; -import org.jkiss.dbeaver.utils.RuntimeUtils; +import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor; +import org.jkiss.dbeaver.registry.settings.ProductSettingsRegistry; import org.jkiss.utils.CommonUtils; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; /** @@ -76,14 +74,25 @@ public class WebServiceCore implements DBWServiceCore { private static final Log log = Log.getLog(WebServiceCore.class); @Override - public WebServerConfig getServerConfig() { - return new WebServerConfig(CBApplication.getInstance()); + public WebServerConfig getServerConfig(@Nullable WebSession webSession) { + WebServerConfig webServerConfig = WebAppUtils.getWebApplication().getWebServerConfig(); + webServerConfig.setProvideSensitiveInformation(webServerConfig.isConfigurationMode() || + (webSession != null && webSession.getUser() != null)); + return webServerConfig; + } + + @Override + public WebPropertyInfo[] getSystemInformationProperties(@NotNull WebSession webSession) { + return WebCommonUtils.getObjectProperties( + webSession, + WebAppUtils.getWebApplication().getSystemInformationCollector() + ); } @Override public List getDriverList(@NotNull WebSession webSession, String driverId) { List result = new ArrayList<>(); - for (DBPDriver driver : CBPlatform.getInstance().getApplicableDrivers()) { + for (DBPDriver driver : WebAppUtils.getWebApplication().getDriverRegistry().getApplicableDrivers()) { if (driverId == null || driverId.equals(driver.getFullId())) { result.add(new WebDatabaseDriverInfo(webSession, driver)); } @@ -100,6 +109,7 @@ public List getAuthModels(@NotNull WebSession webSession) @Override public List getNetworkHandlers(@NotNull WebSession webSession) { return NetworkHandlerRegistry.getInstance().getDescriptors().stream() + .filter(d -> !d.isDesktopHandler()) .map(d -> new WebNetworkHandlerDescriptor(webSession, d)).collect(Collectors.toList()); } @@ -116,68 +126,18 @@ public List getUserConnections( return Collections.singletonList(connectionInfo); } } - var stream = webSession.getConnections().stream(); + var stream = webSession.getAccessibleProjects().stream(); if (projectId != null) { - stream = stream.filter(c -> c.getProjectId().equals(projectId)); + stream = stream.filter(c -> c.getId().equals(projectId)); } if (projectIds != null) { - stream = stream.filter(c -> projectIds.contains(c.getProjectId())); - } - List applicableDrivers = CBPlatform.getInstance().getApplicableDrivers(); - return stream.filter(c -> applicableDrivers.contains(c.getDataSourceContainer().getDriver())) - .collect(Collectors.toList()); - } - - @Deprecated - @Override - public List getTemplateDataSources() throws DBWebException { - - List result = new ArrayList<>(); - DBPDataSourceRegistry dsRegistry = WebServiceUtils.getGlobalDataSourceRegistry(); - - for (DBPDataSourceContainer ds : dsRegistry.getDataSources()) { - if (ds.isTemplate()) { - if (CBPlatform.getInstance().getApplicableDrivers().contains(ds.getDriver())) { - result.add(new WebDataSourceConfig(ds)); - } else { - log.debug("Template datasource '" + ds.getName() + "' ignored - driver is not applicable"); - } - } - } - - return result; - } - - @Override - public List getTemplateConnections( - @NotNull WebSession webSession, @Nullable String projectId - ) throws DBWebException { - List result = new ArrayList<>(); - if (projectId == null) { - for (DBPProject project : webSession.getAccessibleProjects()) { - getTemplateConnectionsFromProject(webSession, project, result); - } - } else { - DBPProject project = getProjectById(webSession, projectId); - getTemplateConnectionsFromProject(webSession, project, result); - } - webSession.filterAccessibleConnections(result); - return result; - } - - private void getTemplateConnectionsFromProject( - @NotNull WebSession webSession, - @NotNull DBPProject project, - List result - ) { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - for (DBPDataSourceContainer ds : registry.getDataSources()) { - if (ds.isTemplate() && - CBPlatform.getInstance().getApplicableDrivers().contains(ds.getDriver())) - { - result.add(new WebConnectionInfo(webSession, ds)); - } + stream = stream.filter(c -> projectIds.contains(c.getId())); } + Set applicableDrivers = WebServiceUtils.getApplicableDriversIds(); + return stream + .flatMap(p -> p.getConnections().stream()) + .filter(c -> applicableDrivers.contains(c.getDataSourceContainer().getDriver().getId())) + .toList(); } @Override @@ -202,13 +162,13 @@ private List getConnectionFoldersFromProject( @NotNull DBPProject project ) { return project.getDataSourceRegistry().getAllFolders().stream() - .map(f -> new WebConnectionFolderInfo(webSession, f)).collect(Collectors.toList()); + .map(f -> new WebConnectionFolderInfo(webSession, f)).collect(Collectors.toList()); } @Override public String[] getSessionPermissions(@NotNull WebSession webSession) throws DBWebException { - if (CBApplication.getInstance().isConfigurationMode()) { - return new String[] { + if (ServletAppUtils.getServletApplication().isConfigurationMode()) { + return new String[]{ DBWConstants.PERMISSION_ADMIN }; } @@ -220,8 +180,8 @@ public WebSession openSession( @NotNull WebSession webSession, @Nullable String defaultLocale, @NotNull HttpServletRequest servletRequest, - @NotNull HttpServletResponse servletResponse) throws DBWebException - { + @NotNull HttpServletResponse servletResponse + ) throws DBWebException { for (WebSessionHandlerDescriptor hd : WebHandlerRegistry.getInstance().getSessionHandlers()) { try { hd.getInstance().handleSessionOpen(webSession, servletRequest, servletResponse); @@ -251,16 +211,19 @@ public WebSession getSessionState(@NotNull WebSession webSession) throws DBWebEx } @Override - public List readSessionLog(@NotNull WebSession webSession, Integer maxEntries, Boolean clearEntries) { + public List readSessionLog( + @NotNull WebSession webSession, + Integer maxEntries, + Boolean clearEntries + ) { return webSession.readLog(maxEntries, clearEntries); } @Override public boolean closeSession(HttpServletRequest request) throws DBWebException { try { - var baseWebSession = CBPlatform.getInstance().getSessionManager().closeSession(request); - if (baseWebSession instanceof WebSession) { - var webSession = (WebSession) baseWebSession; + var baseWebSession = WebAppUtils.getWebApplication().getSessionManager().closeSession(request); + if (baseWebSession instanceof WebSession webSession) { for (WebSessionHandlerDescriptor hd : WebHandlerRegistry.getInstance().getSessionHandlers()) { try { hd.getInstance().handleSessionClose(webSession); @@ -278,13 +241,24 @@ public boolean closeSession(HttpServletRequest request) throws DBWebException { } @Override + @Deprecated public boolean touchSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws DBWebException { - return CBPlatform.getInstance().getSessionManager().touchSession(request, response); + return WebAppUtils.getWebApplication().getSessionManager().touchSession(request, response); + } + + @Override + @Deprecated + public WebSession updateSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) + throws DBWebException { + var sessionManager = WebAppUtils.getWebApplication().getSessionManager(); + sessionManager.touchSession(request, response); + return sessionManager.getWebSession(request, response, true); } @Override - public boolean refreshSessionConnections(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws DBWebException { - WebSession session = CBPlatform.getInstance().getSessionManager().getWebSession(request, response); + public boolean refreshSessionConnections(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) + throws DBWebException { + WebSession session = WebAppUtils.getWebApplication().getSessionManager().getWebSession(request, response); if (session == null) { return false; } else { @@ -302,9 +276,11 @@ public boolean changeSessionLanguage(@NotNull WebSession webSession, String loca @Override public WebConnectionInfo getConnectionState( - WebSession webSession, @Nullable String projectId, String connectionId + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull String connectionId ) throws DBWebException { - return webSession.getWebConnectionInfo(projectId, connectionId); + return WebAppUtils.getWebApplication().getConnectionController().getConnectionState(webSession, projectId, connectionId); } @@ -315,72 +291,12 @@ public WebConnectionInfo initConnection( @NotNull String connectionId, @NotNull Map authProperties, @Nullable List networkCredentials, - @Nullable Boolean saveCredentials, - @Nullable Boolean sharedCredentials + boolean saveCredentials, + boolean sharedCredentials, + @Nullable String selectedSecretId ) throws DBWebException { - WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, connectionId); - connectionInfo.setSavedCredentials(authProperties, networkCredentials); - - DBPDataSourceContainer dataSourceContainer = connectionInfo.getDataSourceContainer(); - if (dataSourceContainer.isConnected()) { - throw new DBWebException("Datasource '" + dataSourceContainer.getName() + "' is already connected"); - } - - boolean oldSavePassword = dataSourceContainer.isSavePassword(); - try { - dataSourceContainer.connect(webSession.getProgressMonitor(), true, false); - } catch (Exception e) { - throw new DBWebException("Error connecting to database", e); - } finally { - dataSourceContainer.setSavePassword(oldSavePassword); - connectionInfo.clearCache(); - } - // Mark all specified network configs as saved - boolean[] saveConfig = new boolean[1]; - - if (networkCredentials != null) { - networkCredentials.forEach(c -> { - if (CommonUtils.toBoolean(c.isSavePassword()) && !CommonUtils.isEmpty(c.getUserName())) { - DBWHandlerConfiguration handlerCfg = dataSourceContainer.getConnectionConfiguration().getHandler(c.getId()); - if (handlerCfg != null) { - WebDataSourceUtils.updateHandlerCredentials(handlerCfg, c); - handlerCfg.setSavePassword(true); - saveConfig[0] = true; - } - } - }); - } - if (saveCredentials != null && saveCredentials) { - // Save all passed credentials in the datasource container - WebServiceUtils.saveAuthProperties( - dataSourceContainer, - dataSourceContainer.getConnectionConfiguration(), - authProperties, - true, - sharedCredentials == null ? false : sharedCredentials - ); - - var project = dataSourceContainer.getProject(); - if (project.isUseSecretStorage()) { - try { - dataSourceContainer.persistSecrets(webSession.getUserContext().getSecretController()); - } catch (DBException e) { - throw new DBWebException("Failed to save credentials", e); - } - } - - WebDataSourceUtils.saveCredentialsInDataSource(connectionInfo, dataSourceContainer, dataSourceContainer.getConnectionConfiguration()); - saveConfig[0] = true; - } - if (WebServiceUtils.isGlobalProject(dataSourceContainer.getProject())) { - // Do not flush config for global project (only admin can do it - CB-2415) - saveConfig[0] = false; - } - if (saveConfig[0]) { - dataSourceContainer.persistConfiguration(); - } - - return connectionInfo; + return WebAppUtils.getWebApplication().getConnectionController().initConnection(webSession, projectId, + connectionId, authProperties, networkCredentials, saveCredentials, sharedCredentials, selectedSecretId); } @Override @@ -389,125 +305,37 @@ public WebConnectionInfo createConnection( @Nullable String projectId, @NotNull WebConnectionConfig connectionConfig ) throws DBWebException { - var project = getProjectById(webSession, projectId); - var rmProject = project.getRmProject(); - if (rmProject.getType() == RMProjectType.USER - && !webSession.hasPermission(DBWConstants.PERMISSION_ADMIN) - && !CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections() - ) { - throw new DBWebException("New connection create is restricted by server configuration"); - } - webSession.addInfoMessage("Create new connection"); - DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); - - // we don't need to save credentials for templates - if (connectionConfig.isTemplate()) { - connectionConfig.setSaveCredentials(false); - } - DBPDataSourceContainer newDataSource = WebServiceUtils.createConnectionFromConfig(connectionConfig, sessionRegistry); - if (CommonUtils.isEmpty(newDataSource.getName())) { - newDataSource.setName(CommonUtils.notNull(connectionConfig.getName(), "NewConnection")); - } - - try { - sessionRegistry.addDataSource(newDataSource); - - sessionRegistry.checkForErrors(); - } catch (DBException e) { - sessionRegistry.removeDataSource(newDataSource); - throw new DBWebException("Failed to create connection", e.getCause()); - } - - WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, newDataSource); - webSession.addConnection(connectionInfo); - webSession.addInfoMessage("New connection was created - " + WebServiceUtils.getConnectionContainerInfo(newDataSource)); - WebEventUtils.addDataSourceUpdatedEvent( - webSession.getProjectById(projectId), + DBPDataSourceContainer dataSourceContainer = + WebAppUtils.getWebApplication().getConnectionController().createDataSourceContainer(webSession, projectId, connectionConfig); + return WebAppUtils.getWebApplication().getConnectionController().createConnection( webSession, - connectionInfo.getId(), - WSConstants.EventAction.CREATE, - WSDataSourceProperty.CONFIGURATION + projectId, + dataSourceContainer.getRegistry(), + dataSourceContainer ); - return connectionInfo; } @Override public WebConnectionInfo updateConnection( @NotNull WebSession webSession, @Nullable String projectId, - @NotNull WebConnectionConfig config) throws DBWebException { - // Do not check for custom connection option. Already created connections can be edited. - // Also template connections can be edited -// if (!CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections()) { -// throw new DBWebException("Connection edit is restricted by server configuration"); -// } - DBPDataSourceRegistry sessionRegistry = getProjectById(webSession, projectId).getDataSourceRegistry(); - - WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, config.getConnectionId()); - DBPDataSourceContainer dataSource = connectionInfo.getDataSourceContainer(); - webSession.addInfoMessage("Update connection - " + WebServiceUtils.getConnectionContainerInfo(dataSource)); - var oldDataSource = new DataSourceDescriptor((DataSourceDescriptor) dataSource, dataSource.getRegistry()); - - if (!CommonUtils.isEmpty(config.getName())) { - dataSource.setName(config.getName()); - } - - if (config.getDescription() != null) { - dataSource.setDescription(config.getDescription()); - } - - dataSource.setFolder(config.getFolder() != null ? sessionRegistry.getFolder(config.getFolder()) : null); - - WebServiceUtils.setConnectionConfiguration(dataSource.getDriver(), dataSource.getConnectionConfiguration(), config); - - // we should check that the config has changed but not check for password changes - dataSource.setSharedCredentials(config.isSharedCredentials()); - dataSource.setSavePassword(config.isSaveCredentials()); - boolean sharedCredentials = dataSource.isSharedCredentials() || !dataSource.getProject() - .isUseSecretStorage() && dataSource.isSavePassword(); - if (sharedCredentials) { - //we must notify about the shared password change - WebServiceUtils.saveAuthProperties( - dataSource, - dataSource.getConnectionConfiguration(), - config.getCredentials(), - config.isSaveCredentials(), - config.isSharedCredentials() - ); - } - boolean sendEvent = !((DataSourceDescriptor) dataSource).equalSettings(oldDataSource); - if (!sharedCredentials) { - // secret controller is responsible for notification, password changes applied after checks - WebServiceUtils.saveAuthProperties( - dataSource, - dataSource.getConnectionConfiguration(), - config.getCredentials(), - config.isSaveCredentials(), - config.isSharedCredentials() - ); - } - - WSDataSourceProperty property = getDatasourceEventProperty(oldDataSource, dataSource); - - try { - sessionRegistry.updateDataSource(dataSource); - sessionRegistry.checkForErrors(); - } catch (DBException e) { - throw new DBWebException("Failed to update connection", e); - } - if (sendEvent) { - WebEventUtils.addDataSourceUpdatedEvent( - webSession.getProjectById(projectId), - webSession, - connectionInfo.getId(), - WSConstants.EventAction.UPDATE, - property - ); - } - return connectionInfo; + @NotNull WebConnectionConfig config + ) throws DBWebException { + DBPDataSourceContainer dataSourceContainer = + WebAppUtils.getWebApplication().getConnectionController().getDatasourceConnection(webSession, projectId, config); + return WebAppUtils.getWebApplication().getConnectionController().updateConnection( + webSession, + projectId, + config, + dataSourceContainer, + dataSourceContainer.getRegistry() + ); } - private WSDataSourceProperty getDatasourceEventProperty(DataSourceDescriptor oldDataSource, DBPDataSourceContainer dataSource) { + private WSDataSourceProperty getDatasourceEventProperty( + DataSourceDescriptor oldDataSource, + DBPDataSourceContainer dataSource + ) { if (!oldDataSource.equalConfiguration((DataSourceDescriptor) dataSource)) { return WSDataSourceProperty.CONFIGURATION; } @@ -525,56 +353,8 @@ private WSDataSourceProperty getDatasourceEventProperty(DataSourceDescriptor old public boolean deleteConnection( @NotNull WebSession webSession, @Nullable String projectId, @NotNull String connectionId ) throws DBWebException { - WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, connectionId); - if (connectionInfo.getDataSourceContainer().getProject() != getProjectById(webSession, projectId)) { - throw new DBWebException("Global connection '" + connectionInfo.getName() + "' configuration cannot be deleted"); - } - webSession.addInfoMessage("Delete connection - " + - WebServiceUtils.getConnectionContainerInfo(connectionInfo.getDataSourceContainer())); - closeAndDeleteConnection(webSession, projectId, connectionId, true); - WebEventUtils.addDataSourceUpdatedEvent( - webSession.getProjectById(projectId), - webSession, - connectionId, - WSConstants.EventAction.DELETE, - WSDataSourceProperty.CONFIGURATION - ); - return true; - } - - @Override - public WebConnectionInfo createConnectionFromTemplate( - @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull String templateId, - @Nullable String connectionName) throws DBWebException - { - DBPDataSourceRegistry templateRegistry = getProjectById(webSession, projectId).getDataSourceRegistry(); - DBPDataSourceContainer dataSourceTemplate = templateRegistry.getDataSource(templateId); - if (dataSourceTemplate == null) { - throw new DBWebException("Template data source '" + templateId + "' not found"); - } - - DBPDataSourceRegistry projectRegistry = webSession.getSingletonProject().getDataSourceRegistry(); - DBPDataSourceContainer newDataSource = projectRegistry.createDataSource(dataSourceTemplate); - - ((DataSourceDescriptor) newDataSource).setNavigatorSettings( - CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings()); - - if (!CommonUtils.isEmpty(connectionName)) { - newDataSource.setName(connectionName); - } - try { - projectRegistry.addDataSource(newDataSource); + return WebAppUtils.getWebApplication().getConnectionController().deleteConnection(webSession, projectId, connectionId); - projectRegistry.checkForErrors(); - } catch (DBException e) { - throw new DBWebException(e.getMessage(), e.getCause()); - } - - WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, newDataSource); - webSession.addConnection(connectionInfo); - return connectionInfo; } @Override @@ -582,10 +362,12 @@ public WebConnectionInfo copyConnectionFromNode( @NotNull WebSession webSession, @Nullable String projectId, @NotNull String nodePath, - @NotNull WebConnectionConfig config) throws DBWebException { + @NotNull WebConnectionConfig config + ) throws DBWebException { try { - DBNModel navigatorModel = webSession.getNavigatorModel(); - DBPDataSourceRegistry dataSourceRegistry = getProjectById(webSession, projectId).getDataSourceRegistry(); + DBNModel navigatorModel = webSession.getNavigatorModelOrThrow(); + WebSessionProjectImpl project = getProjectById(webSession, projectId); + DBPDataSourceRegistry dataSourceRegistry = project.getDataSourceRegistry(); DBNNode srcNode = navigatorModel.getNodeByPath(webSession.getProgressMonitor(), nodePath); if (srcNode == null) { @@ -594,12 +376,15 @@ public WebConnectionInfo copyConnectionFromNode( if (!(srcNode instanceof DBNDataSource)) { throw new DBException("Node '" + nodePath + "' is not a datasource node"); } - DBPDataSourceContainer dataSourceTemplate = ((DBNDataSource)srcNode).getDataSourceContainer(); + DBPDataSourceContainer dataSourceTemplate = ((DBNDataSource) srcNode).getDataSourceContainer(); DBPDataSourceContainer newDataSource = dataSourceRegistry.createDataSource(dataSourceTemplate); - ((DataSourceDescriptor) newDataSource).setNavigatorSettings( - CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings()); + ServletApplication app = ServletAppUtils.getServletApplication(); + if (app instanceof WebApplication webApplication) { + ((DataSourceDescriptor) newDataSource).setNavigatorSettings( + webApplication.getAppConfiguration().getDefaultNavigatorSettings()); + } // Copy props from config if (!CommonUtils.isEmpty(config.getName())) { @@ -611,17 +396,8 @@ public WebConnectionInfo copyConnectionFromNode( dataSourceRegistry.addDataSource(newDataSource); - WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, newDataSource); dataSourceRegistry.checkForErrors(); - webSession.addConnection(connectionInfo); - WebEventUtils.addDataSourceUpdatedEvent( - webSession.getProjectById(projectId), - webSession, - connectionInfo.getId(), - WSConstants.EventAction.CREATE, - WSDataSourceProperty.CONFIGURATION - ); - return connectionInfo; + return project.addConnection(newDataSource); } catch (DBException e) { throw new DBWebException("Error copying connection", e); } @@ -629,82 +405,36 @@ public WebConnectionInfo copyConnectionFromNode( @Override public WebConnectionInfo testConnection( - @NotNull WebSession webSession, @Nullable String projectId, @NotNull WebConnectionConfig connectionConfig + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull WebConnectionConfig connectionConfig ) throws DBWebException { - String connectionId = connectionConfig.getConnectionId(); - - connectionConfig.setSaveCredentials(true); // It is used in createConnectionFromConfig - - DBPDataSourceContainer dataSource = WebDataSourceUtils.getLocalOrGlobalDataSource( - CBApplication.getInstance(), webSession, projectId, connectionId); - - WebProjectImpl project = getProjectById(webSession, projectId); - DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); - DBPDataSourceContainer testDataSource; - if (dataSource != null) { - try { - // Check that creds are saved to trigger secrets resolve - dataSource.isCredentialsSaved(); - } catch (DBException e) { - throw new DBWebException("Can't determine whether datasource credentials are saved", e); - } - - testDataSource = dataSource.createCopy(dataSource.getRegistry()); - WebServiceUtils.setConnectionConfiguration( - testDataSource.getDriver(), - testDataSource.getConnectionConfiguration(), - connectionConfig - ); - WebServiceUtils.saveAuthProperties( - testDataSource, - testDataSource.getConnectionConfiguration(), - connectionConfig.getCredentials(), - true, - false, - true - ); - } else { - testDataSource = WebServiceUtils.createConnectionFromConfig(connectionConfig, sessionRegistry); - } - webSession.provideAuthParameters(webSession.getProgressMonitor(), testDataSource, testDataSource.getConnectionConfiguration()); - testDataSource.setSavePassword(true); // We need for test to avoid password callback - if (DataSourceDescriptor.class.isAssignableFrom(testDataSource.getClass())) { - ((DataSourceDescriptor) testDataSource).setAccessCheckRequired(!webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)); - } - try { - ConnectionTestJob ct = new ConnectionTestJob(testDataSource, param -> { - }); - ct.run(webSession.getProgressMonitor()); - if (ct.getConnectError() != null) { - throw new DBWebException("Connection failed", ct.getConnectError()); - } - WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, testDataSource); - connectionInfo.setConnectError(ct.getConnectError()); - connectionInfo.setServerVersion(ct.getServerVersion()); - connectionInfo.setClientVersion(ct.getClientVersion()); - connectionInfo.setConnectTime(RuntimeUtils.formatExecutionTime(ct.getConnectTime())); - return connectionInfo; - } catch (DBException e) { - throw new DBWebException("Error connecting to database", e); - } + DataSourceDescriptor dataSourceDescriptor = WebAppUtils.getWebApplication().getConnectionController() + .prepareTestConnection(webSession, projectId, connectionConfig); + return WebAppUtils.getWebApplication().getConnectionController() + .testConnection(webSession, projectId, connectionConfig, dataSourceDescriptor); } @Override - public WebNetworkEndpointInfo testNetworkHandler(@NotNull WebSession webSession, @NotNull WebNetworkHandlerConfigInput nhConfig) throws DBWebException { + public WebNetworkEndpointInfo testNetworkHandler( + @NotNull WebSession webSession, + @NotNull WebNetworkHandlerConfigInput nhConfig + ) throws DBWebException { DBRProgressMonitor monitor = webSession.getProgressMonitor(); monitor.beginTask("Instantiate SSH tunnel", 2); - NetworkHandlerDescriptor handlerDescriptor = NetworkHandlerRegistry.getInstance().getDescriptor(nhConfig.getId()); + NetworkHandlerDescriptor handlerDescriptor = NetworkHandlerRegistry.getInstance() + .getDescriptor(nhConfig.getId()); if (handlerDescriptor == null) { throw new DBWebException("Network handler '" + nhConfig.getId() + "' not found"); } try { DBWNetworkHandler handler = handlerDescriptor.createHandler(DBWNetworkHandler.class); - if (handler instanceof DBWTunnel) { - DBWTunnel tunnel = (DBWTunnel)handler; + if (handler instanceof DBWTunnel tunnel) { DBPConnectionConfiguration connectionConfig = new DBPConnectionConfiguration(); connectionConfig.setHostName(DBConstants.HOST_LOCALHOST); - connectionConfig.setHostPort(CommonUtils.toString(nhConfig.getProperties().get(DBWHandlerConfiguration.PROP_PORT))); + connectionConfig.setHostPort(CommonUtils.toString(nhConfig.getProperties() + .get(DBWHandlerConfiguration.PROP_PORT))); try { monitor.subTask("Initialize tunnel"); @@ -715,12 +445,11 @@ public WebNetworkEndpointInfo testNetworkHandler(@NotNull WebSession webSession, tunnel.initializeHandler(monitor, configuration, connectionConfig); monitor.worked(1); // Get info - Object implementation = tunnel.getImplementation(); - if (implementation instanceof SSHImplementation) { + if (tunnel.getImplementation() instanceof SSHSession session) { return new WebNetworkEndpointInfo( "Connected", - ((SSHImplementation) implementation).getClientVersion(), - ((SSHImplementation) implementation).getServerVersion()); + session.getClientVersion(), + session.getServerVersion()); } else { return new WebNetworkEndpointInfo("Connected"); } @@ -754,12 +483,13 @@ private WebConnectionInfo closeAndDeleteConnection( @NotNull String connectionId, boolean forceDelete ) throws DBWebException { - WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, connectionId); + WebSessionProjectImpl project = getProjectById(webSession, projectId); + WebConnectionInfo connectionInfo = project.getWebConnectionInfo(connectionId); DBPDataSourceContainer dataSourceContainer = connectionInfo.getDataSourceContainer(); boolean disconnected = WebDataSourceUtils.disconnectDataSource(webSession, dataSourceContainer); if (forceDelete) { - DBPDataSourceRegistry registry = getProjectById(webSession, projectId).getDataSourceRegistry(); + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); registry.removeDataSource(dataSourceContainer); try { registry.checkForErrors(); @@ -771,7 +501,7 @@ private WebConnectionInfo closeAndDeleteConnection( } throw new DBWebException("Failed to delete connection", e); } - webSession.removeConnection(connectionInfo); + project.removeConnection(dataSourceContainer); } else { // Just reset saved credentials connectionInfo.clearCache(); @@ -784,9 +514,10 @@ private WebConnectionInfo closeAndDeleteConnection( @Override public List getProjects(@NotNull WebSession session) { var customConnectionsEnabled = - CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections() + ServletAppUtils.getServletApplication().getAppConfiguration().isSupportsCustomConnections() || SMUtils.isRMAdmin(session); return session.getAccessibleProjects().stream() + .filter(pr -> customConnectionsEnabled || !RMProjectType.USER.equals(pr.getRMProject().getType())) .map(pr -> new WebProjectInfo(session, pr, customConnectionsEnabled)) .collect(Collectors.toList()); } @@ -799,26 +530,20 @@ public WebConnectionFolderInfo createConnectionFolder( @Nullable String parentPath, @NotNull String folderName ) throws DBWebException { - DBRProgressMonitor monitor = session.getProgressMonitor(); WebConnectionFolderUtils.validateConnectionFolder(folderName); session.addInfoMessage("Create new folder"); - WebConnectionFolderInfo parentNode = null; + WebConnectionFolderInfo parentFolder = null; try { if (parentPath != null) { - parentNode = WebConnectionFolderUtils.getFolderInfo(session, projectId, parentPath); + parentFolder = WebConnectionFolderUtils.getFolderInfo(session, projectId, parentPath); } WebProjectImpl project = getProjectById(session, projectId); - DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); - DBPDataSourceFolder newFolder = WebConnectionFolderUtils.createFolder(parentNode, folderName, sessionRegistry); - WebConnectionFolderInfo folderInfo = new WebConnectionFolderInfo(session, newFolder); - WebServiceUtils.updateConfigAndRefreshDatabases(session, projectId); - WebEventUtils.addNavigatorNodeUpdatedEvent( - session.getProjectById(projectId), - session, - DBNLocalFolder.makeLocalFolderItemPath(newFolder), - WSConstants.EventAction.CREATE + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + DBPDataSourceFolder newFolder = registry.addFolder( + parentFolder == null ? null : parentFolder.getDataSourceFolder(), folderName ); - return folderInfo; + WebServiceUtils.refreshDatabases(session, projectId); + return new WebConnectionFolderInfo(session, newFolder); } catch (DBException e) { throw new DBWebException(e.getMessage(), e); } @@ -829,26 +554,12 @@ public WebConnectionFolderInfo renameConnectionFolder( @NotNull WebSession session, @Nullable String projectId, @NotNull String folderPath, - @NotNull String newName + @NotNull String newPath ) throws DBWebException { - WebConnectionFolderUtils.validateConnectionFolder(newName); + WebConnectionFolderUtils.validateConnectionFolder(newPath); WebConnectionFolderInfo folderInfo = WebConnectionFolderUtils.getFolderInfo(session, projectId, folderPath); - var oldFolderNode = DBNLocalFolder.makeLocalFolderItemPath(folderInfo.getDataSourceFolder()); - folderInfo.getDataSourceFolder().setName(newName); - var newFolderNode = DBNLocalFolder.makeLocalFolderItemPath(folderInfo.getDataSourceFolder()); - WebServiceUtils.updateConfigAndRefreshDatabases(session, projectId); - WebEventUtils.addNavigatorNodeUpdatedEvent( - session.getProjectById(projectId), - session, - oldFolderNode, - WSConstants.EventAction.DELETE - ); - WebEventUtils.addNavigatorNodeUpdatedEvent( - session.getProjectById(projectId), - session, - newFolderNode, - WSConstants.EventAction.CREATE - ); + folderInfo.getDataSourceFolder().setName(newPath); + WebServiceUtils.refreshDatabases(session, projectId); return folderInfo; } @@ -863,17 +574,10 @@ public boolean deleteConnectionFolder( if (folder.getDataSourceRegistry().getProject() != project) { throw new DBWebException("Global folder '" + folderInfo.getId() + "' cannot be deleted"); } - var folderNode = DBNLocalFolder.makeLocalFolderItemPath(folderInfo.getDataSourceFolder()); session.addInfoMessage("Delete folder"); DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); sessionRegistry.removeFolder(folderInfo.getDataSourceFolder(), false); - WebServiceUtils.updateConfigAndRefreshDatabases(session, projectId); - WebEventUtils.addNavigatorNodeUpdatedEvent( - session.getProjectById(projectId), - session, - folderNode, - WSConstants.EventAction.DELETE - ); + WebServiceUtils.refreshDatabases(session, projectId); } catch (DBException e) { throw new DBWebException(e.getMessage(), e); } @@ -884,21 +588,16 @@ public boolean deleteConnectionFolder( public WebConnectionInfo setConnectionNavigatorSettings( WebSession webSession, @Nullable String projectId, String id, DBNBrowseSettings settings ) throws DBWebException { - WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, id); - DataSourceDescriptor dataSourceDescriptor = ((DataSourceDescriptor)connectionInfo.getDataSourceContainer()); + WebConnectionInfo connectionInfo = WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, id); + DataSourceDescriptor dataSourceDescriptor = ((DataSourceDescriptor) connectionInfo.getDataSourceContainer()); dataSourceDescriptor.setNavigatorSettings(settings); dataSourceDescriptor.persistConfiguration(); - WebEventUtils.addDataSourceUpdatedEvent( - webSession.getProjectById(projectId), - webSession, - id, - WSConstants.EventAction.UPDATE, - WSDataSourceProperty.CONFIGURATION); return connectionInfo; } @Override - public WebAsyncTaskInfo getAsyncTaskInfo(WebSession webSession, String taskId, Boolean removeOnFinish) throws DBWebException { + public WebAsyncTaskInfo getAsyncTaskInfo(WebSession webSession, String taskId, Boolean removeOnFinish) + throws DBWebException { return webSession.asyncTaskStatus(taskId, CommonUtils.toBoolean(removeOnFinish)); } @@ -907,8 +606,13 @@ public boolean cancelAsyncTask(WebSession webSession, String taskId) throws DBWe return webSession.asyncTaskCancel(taskId); } - private WebProjectImpl getProjectById(WebSession webSession, String projectId) throws DBWebException { - WebProjectImpl project = webSession.getProjectById(projectId); + @Override + public WebGroupPropertiesInfo getProductSettings(@NotNull WebSession webSession) { + return new WebGroupPropertiesInfo<>(webSession, ProductSettingsRegistry.getInstance().getSettings()); + } + + private WebSessionProjectImpl getProjectById(WebSession webSession, String projectId) throws DBWebException { + WebSessionProjectImpl project = webSession.getProjectById(projectId); if (project == null) { throw new DBWebException("Project '" + projectId + "' not found"); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/DBWFeatureProvider.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/DBWFeatureProvider.java new file mode 100644 index 0000000000..93d805cd78 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/DBWFeatureProvider.java @@ -0,0 +1,35 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.navigator; + +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.navigator.DBNNode; + +import java.util.List; + +/** + * DBWFeatureProvider + * This interface is used to provide features for database objects in the context of the CloudBeaver web service. + * Implementations should return an array of feature strings that describe the capabilities or characteristics of the given object. + */ +public interface DBWFeatureProvider { + + @NotNull + List getNodeFeatures(@NotNull WebSession webSession, @NotNull DBNNode node); + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/DBWServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/DBWServiceNavigator.java index 113288cb11..78d0c1fb3a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/DBWServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/DBWServiceNavigator.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -57,9 +57,10 @@ boolean setNavigatorNodeFilter( @Nullable List exclude) throws DBWebException; @WebAction - boolean refreshNavigatorNode( + WebNavigatorNodeInfo refreshNavigatorNode( @NotNull WebSession session, - @NotNull String nodePath) throws DBWebException; + @NotNull String nodePath, + @Nullable Boolean recursive) throws DBWebException; @WebAction WebStructContainers getStructContainers( diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebCatalog.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebCatalog.java index 6cbd2a7b90..d25bd47a85 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebCatalog.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebCatalog.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java index 9dd8ce89b4..7fe7cd1d01 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,21 +16,24 @@ */ package io.cloudbeaver.service.navigator; +import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.WebPropertyInfo; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.service.security.SMUtils; +import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.*; import org.jkiss.dbeaver.model.meta.Property; -import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; +import org.jkiss.dbeaver.model.rm.RMProjectPermission; import org.jkiss.dbeaver.model.struct.*; import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog; import org.jkiss.dbeaver.model.struct.rdb.DBSSchema; import org.jkiss.dbeaver.model.struct.rdb.DBSTable; -import org.jkiss.dbeaver.runtime.properties.PropertyCollector; -import org.jkiss.utils.CommonUtils; import java.util.ArrayList; +import java.util.Collection; import java.util.List; @@ -90,26 +93,20 @@ public WebPropertyInfo[] getProperties() { @Property public WebPropertyInfo[] filterProperties(@Nullable WebPropertyFilter filter) { - PropertyCollector propertyCollector = new PropertyCollector(object, true); - propertyCollector.setLocale(session.getLocale()); - propertyCollector.collectProperties(); - List webProps = new ArrayList<>(); - for (DBPPropertyDescriptor prop : propertyCollector.getProperties()) { - if (filter != null && !CommonUtils.isEmpty(filter.getIds()) && !filter.getIds().contains(CommonUtils.toString(prop.getId()))) { - continue; - } - WebPropertyInfo webProperty = new WebPropertyInfo(session, prop, propertyCollector); - if (filter != null) { - if (!CommonUtils.isEmpty(filter.getFeatures()) && !webProperty.hasAnyFeature(filter.getFeatures())) { - continue; - } - if (!CommonUtils.isEmpty(filter.getCategories()) && !filter.getCategories().contains(webProperty.getCategory())) { - continue; - } - } - webProps.add(webProperty); + if (object instanceof DBPDataSourceContainer container && !isDataSourceEditable(container)) { + // If user cannot edit a connection, then return only name + filter = new WebPropertyFilter(); + filter.setFeatures(List.of(DBConstants.PROP_FEATURE_NAME)); + } + return WebServiceUtils.getObjectFilteredProperties(session, object, filter); + } + + private boolean isDataSourceEditable(@NotNull DBPDataSourceContainer container) { + WebProjectImpl project = session.getProjectById(container.getProject().getId()); + if (project == null) { + return false; } - return webProps.toArray(new WebPropertyInfo[0]); + return SMUtils.hasProjectPermission(session, project.getRMProject(), RMProjectPermission.DATA_SOURCES_EDIT); } /////////////////////////////////// @@ -122,7 +119,9 @@ public Integer getOrdinalPosition() { @Property public String getFullyQualifiedName() { - return object instanceof DBPQualifiedObject ? ((DBPQualifiedObject) object).getFullyQualifiedName(DBPEvaluationContext.UI) : getName(); + return object instanceof DBPQualifiedObject + ? ((DBPQualifiedObject) object).getFullyQualifiedName(DBPEvaluationContext.UI) + : getName(); } @Property @@ -169,7 +168,7 @@ public String[] getFeatures() { return features.toArray(new String[0]); } - private static void getObjectFeatures(DBSObject object, List features) { + private void getObjectFeatures(DBSObject object, List features) { boolean isDiagramSupported = true; if (object instanceof DBPScriptObject) features.add(OBJECT_FEATURE_SCRIPT); if (object instanceof DBPScriptObjectExt) features.add(OBJECT_FEATURE_SCRIPT_EXTENDED); @@ -195,10 +194,10 @@ private static void getObjectFeatures(DBSObject object, List features) { } if (object instanceof DBSSchema) features.add(OBJECT_FEATURE_SCHEMA); if (object instanceof DBSCatalog) features.add(OBJECT_FEATURE_CATALOG); - if (object instanceof DBSObjectContainer) { + if (object instanceof DBSObjectContainer objectContainer) { features.add(OBJECT_FEATURE_OBJECT_CONTAINER); try { - Class childType = ((DBSObjectContainer) object).getPrimaryChildType(null); + Class childType = objectContainer.getPrimaryChildType(null); if (DBSTable.class.isAssignableFrom(childType)) { features.add(OBJECT_FEATURE_ENTITY_CONTAINER); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDefaultFeatureProvider.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDefaultFeatureProvider.java new file mode 100644 index 0000000000..f5a7600943 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDefaultFeatureProvider.java @@ -0,0 +1,192 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.navigator; + +import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.model.rm.DBNResourceManagerProject; +import io.cloudbeaver.model.rm.DBNResourceManagerResource; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.registry.WebDriverRegistry; +import io.cloudbeaver.server.WebAppUtils; +import io.cloudbeaver.service.security.SMUtils; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.DBPDataSourceFolder; +import org.jkiss.dbeaver.model.DBUtils; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.model.edit.DBEObjectMaker; +import org.jkiss.dbeaver.model.edit.DBEObjectRenamer; +import org.jkiss.dbeaver.model.navigator.*; +import org.jkiss.dbeaver.model.navigator.fs.DBNPath; +import org.jkiss.dbeaver.model.navigator.meta.DBXTreeNode; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.rm.RMProjectPermission; +import org.jkiss.dbeaver.model.struct.DBSEntity; +import org.jkiss.dbeaver.model.struct.DBSObject; +import org.jkiss.dbeaver.model.struct.rdb.DBSProcedure; +import org.jkiss.dbeaver.registry.ResourceTypeRegistry; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class WebDefaultFeatureProvider implements DBWFeatureProvider { + public static final String NODE_FEATURE_ITEM = "item"; + public static final String NODE_FEATURE_LEAF = "leaf"; + public static final String NODE_FEATURE_CONTAINER = "container"; + public static final String NODE_FEATURE_SHARED = "shared"; + public static final String NODE_FEATURE_CAN_DELETE = "canDelete"; + public static final String NODE_FEATURE_CAN_FILTER = "canFilter"; + public static final String NODE_FEATURE_CAN_RENAME = "canRename"; + public static final String NODE_FEATURE_CAN_CREATE_CONNECTION_FROM_NODE = "canCreateConnectionFromNode"; + + + @NotNull + @Override + public List getNodeFeatures(@NotNull WebSession webSession, @NotNull DBNNode node) { + List features = new ArrayList<>(); + boolean isLeaf = false; + if (node instanceof DBNDatabaseItem databaseItem) { + features.add(NODE_FEATURE_ITEM); + DBSObject object = databaseItem.getObject(); + if (object instanceof DBSEntity || object instanceof DBSProcedure) { + features.add(NODE_FEATURE_LEAF); + isLeaf = true; + } + } + if (node instanceof DBNContainer) { + features.add(NODE_FEATURE_CONTAINER); + } + boolean isShared = false; + if (node instanceof DBNDatabaseNode && !isLeaf) { + if (node instanceof DBNDataSource dataSource) { + if (dataSource.getDataSourceContainer().getDataSource() != null) { + boolean hasNonFolderNode = DBXTreeNode.hasNonFolderNode(dataSource.getMeta().getChildren(null)); + if (hasNonFolderNode) { + features.add(NODE_FEATURE_CAN_FILTER); + } + } + } else if (node instanceof DBNDatabaseItem item) { + if (item.getDataSourceContainer().getDataSource() != null) { + boolean hasNonFolderNode = DBXTreeNode.hasNonFolderNode(item.getMeta().getChildren(null)); + if (hasNonFolderNode) { + features.add(NODE_FEATURE_CAN_FILTER); + } + } + } else { + features.add(NODE_FEATURE_CAN_FILTER); + } + isShared = !node.getOwnerProject().getName().equals(webSession.getUserId()); + } else if (node instanceof DBNLocalFolder dbnLocalFolder) { + DBPDataSourceFolder folder = dbnLocalFolder.getFolder(); + DBPProject project = folder.getDataSourceRegistry().getProject(); + String projectName = project.getName(); + Set tempFolders = folder.getDataSourceRegistry().getTemporaryFolders(); + isShared = !projectName.equals(webSession.getUserId()) || tempFolders.contains(folder); + if (hasNodePermission(webSession, node, RMProjectPermission.DATA_SOURCES_EDIT)) { + features.add(NODE_FEATURE_CAN_RENAME); + features.add(NODE_FEATURE_CAN_DELETE); + } + } + if (isShared) { + features.add(NODE_FEATURE_SHARED); + } + if (node instanceof DBNDatabaseNode) { + boolean canEditDatasources = hasNodePermission(webSession, node, RMProjectPermission.DATA_SOURCES_EDIT); + DBSObject object = ((DBNDatabaseNode) node).getObject(); + if (object != null && canEditDatasources && !DBUtils.isReadOnly(object)) { + DBEObjectMaker objectManager = DBWorkbench.getPlatform().getEditorsRegistry().getObjectManager( + object.getClass(), DBEObjectMaker.class); + if (objectManager != null && objectManager.canDeleteObject(object)) { + features.add(NODE_FEATURE_CAN_DELETE); + } + if (objectManager instanceof DBEObjectRenamer renamer && renamer.canRenameObject(object)) { + if (!object.getDataSource().getContainer().getNavigatorSettings().isShowOnlyEntities()) { + features.add(NODE_FEATURE_CAN_RENAME); + } + } + } + } + if (node instanceof DBNRoot) { + return features; + } + if (node instanceof DBNResourceManagerResource && !isDistributedSpecialFolderNode(webSession, node)) { + if (hasNodePermission(webSession, node, RMProjectPermission.RESOURCE_EDIT)) { + features.add(NODE_FEATURE_CAN_RENAME); + features.add(NODE_FEATURE_CAN_DELETE); + } + } + if (node instanceof DBNPath dbnPath) { + if (canCreateConnectionFromFileName(dbnPath.getName())) { + features.add(NODE_FEATURE_CAN_CREATE_CONNECTION_FROM_NODE); + } + } + return features; + } + + @NotNull + private String getProjectId(@NotNull DBNNode node) { + return node.getOwnerProject().getId(); + } + + private boolean canCreateConnectionFromFileName(String fileName) { + String fileExtension = IOUtils.getFileExtension(fileName); + if (CommonUtils.isEmpty(fileExtension)) { + return false; + } + WebDriverRegistry driverRegistry = WebAppUtils.getWebApplication().getDriverRegistry(); + Set dbpDrivers = driverRegistry.getSupportedFileOpenExtension().get(fileExtension); + if (dbpDrivers == null) { + return false; + } + for (DBPDriver dbpDriver : dbpDrivers) { + if (WebServiceUtils.isDriverEnabled(dbpDriver)) { + return true; + } + } + return false; + } + + private boolean hasNodePermission(@NotNull WebSession webSession, @NotNull DBNNode node, @NotNull RMProjectPermission permission) { + WebProjectImpl project = webSession.getProjectById(node.getOwnerProject().getId()); + if (project == null) { + return false; + } + RMProject rmProject = project.getRMProject(); + return SMUtils.hasProjectPermission(webSession, rmProject, permission); + } + + private boolean isDistributedSpecialFolderNode(@NotNull WebSession webSession, @NotNull DBNNode node) { + // do not send rename/delete features for distributed resource manager special folder + if (!webSession.getApplication().isDistributed() + || !(node instanceof DBNResourceManagerResource resourceNode) + || !WebServiceUtils.isFolder(node) + ) { + return false; + } + // check only root folders + if (!(node.getParentNode() instanceof DBNResourceManagerProject)) { + return false; + } + var folderPath = resourceNode.getResourceFolder(); + return ResourceTypeRegistry.getInstance().getResourceTypeByRootPath(null, folderPath) != null; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java index 14bd0c8436..a641fd9866 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,48 +17,40 @@ package io.cloudbeaver.service.navigator; import io.cloudbeaver.DBWebException; -import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.WebPropertyInfo; -import io.cloudbeaver.model.rm.DBNResourceManagerProject; +import io.cloudbeaver.model.fs.FSUtils; import io.cloudbeaver.model.rm.DBNResourceManagerResource; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.registry.WebObjectFeatureProviderDescriptor; +import io.cloudbeaver.registry.WebObjectFeatureRegistry; import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.*; import org.jkiss.dbeaver.model.app.DBPProject; -import org.jkiss.dbeaver.model.edit.DBEObjectMaker; -import org.jkiss.dbeaver.model.edit.DBEObjectRenamer; +import org.jkiss.dbeaver.model.fs.DBFUtils; import org.jkiss.dbeaver.model.meta.Association; import org.jkiss.dbeaver.model.meta.Property; -import org.jkiss.dbeaver.model.navigator.*; -import org.jkiss.dbeaver.model.rm.RMProject; -import org.jkiss.dbeaver.model.rm.RMProjectPermission; -import org.jkiss.dbeaver.model.struct.DBSEntity; +import org.jkiss.dbeaver.model.navigator.DBNDataSource; +import org.jkiss.dbeaver.model.navigator.DBNDatabaseNode; +import org.jkiss.dbeaver.model.navigator.DBNNode; +import org.jkiss.dbeaver.model.navigator.DBNUtils; +import org.jkiss.dbeaver.model.navigator.fs.DBNFileSystem; +import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase; import org.jkiss.dbeaver.model.struct.DBSObject; import org.jkiss.dbeaver.model.struct.DBSObjectFilter; -import org.jkiss.dbeaver.model.struct.rdb.DBSProcedure; -import org.jkiss.dbeaver.registry.DataSourceFolder; -import org.jkiss.dbeaver.registry.ResourceTypeRegistry; -import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.Set; /** * Web connection info */ public class WebNavigatorNodeInfo { - public static final String NODE_FEATURE_ITEM = "item"; - public static final String NODE_FEATURE_LEAF = "leaf"; - public static final String NODE_FEATURE_CONTAINER = "container"; - public static final String NODE_FEATURE_SHARED = "shared"; - public static final String NODE_FEATURE_CAN_DELETE = "canDelete"; - public static final String NODE_FEATURE_CAN_RENAME = "canRename"; + private static final Log log = Log.getLog(WebNavigatorNodeInfo.class); private final WebSession session; private final DBNNode node; @@ -76,10 +68,16 @@ public DBNNode getNode() { /////////////////////////////////// @Property + @Deprecated(forRemoval = true) public String getId() { return node.getNodeItemPath(); } + @Property + public String getUri() { + return node.getNodeUri(); + } + @Property public String getName() { return node.getLocalizedName(session.getLocale()); @@ -99,7 +97,7 @@ public String getPlainName() { // for renaming node @Property public String getProjectId() { - DBPProject ownerProject = node.getOwnerProject(); + DBPProject ownerProject = node.getOwnerProjectOrNull(); return ownerProject == null ? null : ownerProject.getId(); } @@ -107,11 +105,11 @@ public String getProjectId() { @Deprecated public String getFullName() { String nodeName; - if (node instanceof DBNDatabaseNode && !(node instanceof DBNDataSource)) { - DBSObject object = ((DBNDatabaseNode) node).getObject(); + if (node instanceof DBNDatabaseNode dbNode && !(node instanceof DBNDataSource)) { + DBSObject object = dbNode.getObject(); nodeName = DBUtils.getObjectFullName(object, DBPEvaluationContext.UI); - } else if (node instanceof DBNDataSource) { - DBPDataSourceContainer object = ((DBNDataSource) node).getDataSourceContainer(); + } else if (node instanceof DBNDataSource dataSource) { + DBPDataSourceContainer object = dataSource.getDataSourceContainer(); nodeName = object.getName(); } else { nodeName = node.getNodeTargetName(); @@ -136,9 +134,7 @@ public String getNodeType() { @Property public boolean isFolder() { - return (node instanceof DBNContainer && !(node instanceof DBNDataSource)) - || (node instanceof DBNResourceManagerResource - && ((DBNResourceManagerResource) node).getResource().isFolder()); + return WebServiceUtils.isFolder(node); } @Property @@ -168,93 +164,22 @@ public boolean isHasChildren() { @Association public String[] getFeatures() { List features = new ArrayList<>(); - if (node instanceof DBNDatabaseItem) { - features.add(NODE_FEATURE_ITEM); - DBSObject object = ((DBNDatabaseItem) node).getObject(); - if (object instanceof DBSEntity || object instanceof DBSProcedure) { - features.add(NODE_FEATURE_LEAF); - } - - } - if (node instanceof DBNContainer) { - features.add(NODE_FEATURE_CONTAINER); - } - boolean isShared = false; - if (node instanceof DBNDatabaseNode) { - isShared = !((DBNDatabaseNode) node).getOwnerProject().getName().equals(session.getUserId()); - } else if (node instanceof DBNLocalFolder) { - DataSourceFolder folder = (DataSourceFolder) ((DBNLocalFolder) node).getFolder(); - DBPProject project = folder.getDataSourceRegistry().getProject(); - String projectName = project.getName(); - Set tempFolders = folder.getDataSourceRegistry().getTemporaryFolders(); - isShared = !projectName.equals(session.getUserId()) || tempFolders.contains(folder); - if (hasNodePermission(RMProjectPermission.DATA_SOURCES_EDIT)) { - features.add(NODE_FEATURE_CAN_RENAME); - features.add(NODE_FEATURE_CAN_DELETE); - } - } - if (isShared) { - features.add(NODE_FEATURE_SHARED); - } - if (node instanceof DBNDatabaseNode) { - boolean canEditDatasources = hasNodePermission(RMProjectPermission.DATA_SOURCES_EDIT); - DBSObject object = ((DBNDatabaseNode) node).getObject(); - if (object != null && canEditDatasources) { - DBEObjectMaker objectManager = DBWorkbench.getPlatform().getEditorsRegistry().getObjectManager( - object.getClass(), DBEObjectMaker.class); - if (objectManager != null && objectManager.canDeleteObject(object)) { - features.add(NODE_FEATURE_CAN_DELETE); - } - if (objectManager instanceof DBEObjectRenamer && ((DBEObjectRenamer) objectManager).canRenameObject(object)) { - if (!object.getDataSource().getContainer().getNavigatorSettings().isShowOnlyEntities()) { - features.add(NODE_FEATURE_CAN_RENAME); - } - } - } - } - if (node instanceof DBNRoot) { - return features.toArray(new String[0]); - } - if (node instanceof DBNResourceManagerResource && !isDistributedSpecialFolderNode()) { - if (hasNodePermission(RMProjectPermission.RESOURCE_EDIT)) { - features.add(NODE_FEATURE_CAN_RENAME); - features.add(NODE_FEATURE_CAN_DELETE); - } + for (WebObjectFeatureProviderDescriptor provider : WebObjectFeatureRegistry.getInstance().getProviders()) { + features.addAll(provider.getInstance().getNodeFeatures(session, node)); } return features.toArray(new String[0]); } - private boolean hasNodePermission(RMProjectPermission permission) { - WebProjectImpl project = session.getProjectById(getProjectId()); - if (project == null) { - return false; - } - RMProject rmProject = project.getRmProject(); - return SMUtils.hasProjectPermission(session, rmProject, permission); - } - - private boolean isDistributedSpecialFolderNode() { - // do not send rename/delete features for distributed resource manager special folder - if (!session.getApplication().isDistributed() || !(node instanceof DBNResourceManagerResource) || !isFolder()) { - return false; - } - // check only root folders - if (!(node.getParentNode() instanceof DBNResourceManagerProject)) { - return false; - } - var folderPath = ((DBNResourceManagerResource) node).getResourceFolder(); - return ResourceTypeRegistry.getInstance().getResourceTypeByRootPath(null, folderPath) != null; - } - /////////////////////////////////// // Details /////////////////////////////////// @Property public WebPropertyInfo[] getNodeDetails() throws DBWebException { - if (node instanceof DBPObjectWithDetails) { + if (node instanceof DBPObjectWithDetails objectWithDetails) { try { - DBPObject objectDetails = ((DBPObjectWithDetails) node).getObjectDetails(session.getProgressMonitor(), session.getSessionContext(), node); + DBPObject objectDetails = objectWithDetails.getObjectDetails( + session.getProgressMonitor(), session.getSessionContext(), node); if (objectDetails != null) { return WebServiceUtils.getObjectProperties(session, objectDetails); } @@ -271,24 +196,40 @@ public WebPropertyInfo[] getNodeDetails() throws DBWebException { @Property public WebDatabaseObjectInfo getObject() { - if (node instanceof DBNDatabaseNode) { - DBSObject object = ((DBNDatabaseNode) node).getObject(); + if (node instanceof DBNDatabaseNode dbNode) { + DBSObject object = dbNode.getObject(); return object == null ? null : new WebDatabaseObjectInfo(session, object); } return null; } + @Property + public String getObjectId() { + if (node instanceof DBNPathBase dbnPath) { + return DBFUtils.getUriFromPath(dbnPath.getPath()).toString(); + } else if (node instanceof DBNFileSystem dbnFs) { + return FSUtils.makeUniqueFsId(dbnFs.getFileSystem()); + } + return null; + } + @Property public DBSObjectFilter getFilter() throws DBWebException { - if (!(node instanceof DBNDatabaseFolder)) { + if (!(node instanceof DBNDatabaseNode dbNode)) { throw new DBWebException("Invalid navigator node type: " + node.getClass().getName()); } - DBSObjectFilter filter = ((DBNDatabaseFolder) node).getNodeFilter(((DBNDatabaseFolder) node).getItemsMeta(), true); - return filter == null || filter.isEmpty() || !filter.isEnabled() ? null : filter; + try { + DBSObjectFilter filter = dbNode.getNodeFilter( + DBNUtils.getValidItemsMeta(session.getProgressMonitor(), dbNode), + true); + return filter == null || filter.isEmpty() || !filter.isEnabled() ? null : filter; + } catch (DBException e) { + throw new DBWebException(e); + } } @Override public String toString() { - return node.getNodeItemPath(); + return node.getNodeUri(); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebPropertyFilter.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebPropertyFilter.java index 4fd0c842be..62559730cd 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebPropertyFilter.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebPropertyFilter.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebServiceBindingNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebServiceBindingNavigator.java index 07a0f8b16e..bfc9619808 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebServiceBindingNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebServiceBindingNavigator.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -53,8 +53,9 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { )) .dataFetcher("navRefreshNode", env -> getService(env).refreshNavigatorNode( getWebSession(env), - env.getArgument("nodePath") - )) + env.getArgument("nodePath"), + false + ) != null) .dataFetcher("navGetStructContainers", env -> getService(env).getStructContainers( getProjectReference(env), getWebConnection(env), @@ -63,6 +64,11 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { )); model.getMutationType() + .dataFetcher("navReloadNode", env -> getService(env).refreshNavigatorNode( + getWebSession(env), + env.getArgument("nodePath"), + true + )) .dataFetcher("navSetFolderFilter", env -> getService(env).setNavigatorNodeFilter( getWebSession(env), env.getArgument("nodePath"), diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebStructContainers.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebStructContainers.java index 8eeb4c1466..f3493c0418 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebStructContainers.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebStructContainers.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java index c4b7235468..3c0c6888b8 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -19,32 +19,33 @@ import io.cloudbeaver.BaseWebProjectImpl; import io.cloudbeaver.DBWebException; -import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.WebCommandContext; import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.rm.DBNAbstractResourceManagerNode; import io.cloudbeaver.model.rm.DBNResourceManagerResource; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.navigator.DBWServiceNavigator; import io.cloudbeaver.service.navigator.WebCatalog; import io.cloudbeaver.service.navigator.WebNavigatorNodeInfo; import io.cloudbeaver.service.navigator.WebStructContainers; import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.utils.ServletAppUtils; import io.cloudbeaver.utils.WebConnectionFolderUtils; import io.cloudbeaver.utils.WebEventUtils; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.*; -import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.edit.DBECommandContext; import org.jkiss.dbeaver.model.edit.DBEObjectMaker; import org.jkiss.dbeaver.model.edit.DBEObjectRenamer; import org.jkiss.dbeaver.model.exec.DBCExecutionContext; import org.jkiss.dbeaver.model.exec.DBCExecutionContextDefaults; +import org.jkiss.dbeaver.model.exec.DBExecUtils; import org.jkiss.dbeaver.model.navigator.*; +import org.jkiss.dbeaver.model.navigator.meta.DBXTreeItem; +import org.jkiss.dbeaver.model.rm.RMControllerProvider; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.rm.RMProjectPermission; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; @@ -54,7 +55,6 @@ import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog; import org.jkiss.dbeaver.model.struct.rdb.DBSSchema; import org.jkiss.dbeaver.model.websocket.WSConstants; -import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceProperty; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.CommonUtils; @@ -84,21 +84,21 @@ public List getNavigatorNodeChildren( DBNNode[] nodeChildren; boolean isRootPath = CommonUtils.isEmpty(parentPath) || "/".equals(parentPath) || ROOT_DATABASES.equals(parentPath); - DBNModel navigatorModel = session.getNavigatorModel(); - List applicableDrivers = CBPlatform.getInstance().getApplicableDrivers(); + DBNModel navigatorModel = session.getNavigatorModelOrThrow(); + Set applicableDrivers = WebServiceUtils.getApplicableDriversIds(); if (isRootPath) { DBNRoot rootNode = navigatorModel.getRoot(); nodeChildren = DBNUtils.getNodeChildrenFiltered(monitor, rootNode, true); } else { DBNNode parentNode = navigatorModel.getNodeByPath(monitor, parentPath); - if (parentNode == null) { - throw new DBWebException("Node '" + parentPath + "' not found"); - } + if (parentNode == null) { + throw new DBWebException("Node '" + parentPath + "' not found"); + } if (!parentNode.hasChildren(false)) { return EMPTY_NODE_LIST; } - if (parentNode instanceof DBNProject) { - parentNode = ((DBNProject) parentNode).getDatabases(); + if (parentNode instanceof DBNProject projectNode) { + parentNode = projectNode.getDatabases(); } nodeChildren = DBNUtils.getNodeChildrenFiltered(monitor, parentNode, false); } @@ -109,19 +109,20 @@ public List getNavigatorNodeChildren( Set nodeIds = new HashSet<>(); // filter duplicate node ids for (DBNNode node : nodeChildren) { - if (node instanceof DBNDatabaseFolder && CommonUtils.isEmpty(((DBNDatabaseFolder) node).getMeta().getChildren(null))) { + if (node instanceof DBNDatabaseFolder folderNode && CommonUtils.isEmpty(folderNode.getMeta().getChildren(null))) { // Skip empty folders. Folder may become empty if their nested elements are provided by UI plugins. continue; } if (!CommonUtils.toBoolean(onlyFolders) || node instanceof DBNContainer) { // Skip connections which are not supported in CB - if (node instanceof DBNDataSource) { - DBPDataSourceContainer container = ((DBNDataSource) node).getDataSourceContainer(); - if (!applicableDrivers.contains(container.getDriver())) { + if (node instanceof DBNDataSource dataSourceNode) { + DBPDataSourceContainer container = dataSourceNode.getDataSourceContainer(); + // compare by id because driver object can be recreated if it was custom or disabled + if (!applicableDrivers.contains(container.getDriver().getId())) { continue; } } - var nodeId = node.getNodeItemPath(); + var nodeId = node.getNodeUri(); if (nodeIds.contains(nodeId)) { session.addWarningMessage( MessageFormat.format("Duplicate child node ''{0}'' was found in parent node ''{1}''", @@ -130,7 +131,13 @@ public List getNavigatorNodeChildren( ); continue; } - nodeIds.add(node.getNodeItemPath()); + var customConnectionsEnabled = + ServletAppUtils.getServletApplication().getAppConfiguration().isSupportsCustomConnections() + || SMUtils.isRMAdmin(session); + if (node instanceof DBNProject project && !customConnectionsEnabled && project.getProject().isPrivateProject()) { + continue; + } + nodeIds.add(node.getNodeUri()); result.add(new WebNavigatorNodeInfo(session, node)); } } @@ -143,7 +150,7 @@ public List getNavigatorNodeChildren( return result.subList(offset, result.size()); } } catch (DBException e) { - throw new DBWebException(e, null); + throw new DBWebException(e.getMessage(), e); } } @@ -155,8 +162,7 @@ public List getNavigatorNodeParents( try { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNModel navigatorModel = session.getNavigatorModel(); - DBNNode node = navigatorModel.getNodeByPath(monitor, nodePath); + DBNNode node = session.getNavigatorModelOrThrow().getNodeByPath(monitor, nodePath); if (node == null) { throw new DBWebException("Node '" + nodePath + "' not found"); } @@ -178,7 +184,7 @@ public List getNavigatorNodeParents( return nodeParents; } catch (DBException e) { - throw new DBWebException(e, null); + throw new DBWebException(e); } } @@ -198,7 +204,7 @@ public WebNavigatorNodeInfo getNavigatorNodeInfo( try { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNNode node = session.getNavigatorModel().getNodeByPath(monitor, nodePath); + DBNNode node = session.getNavigatorModelOrThrow().getNodeByPath(monitor, nodePath); if (node == null) { throw new DBWebException("Navigator node '" + nodePath + "' not found"); } @@ -217,13 +223,10 @@ public boolean setNavigatorNodeFilter( try { DBRProgressMonitor monitor = webSession.getProgressMonitor(); - DBNNode node = webSession.getNavigatorModel().getNodeByPath(monitor, nodePath); + DBNNode node = webSession.getNavigatorModelOrThrow().getNodeByPath(monitor, nodePath); if (node == null) { throw new DBWebException("Navigator node '" + nodePath + "' not found"); } - if (!(node instanceof DBNDatabaseFolder)) { - throw new DBWebException("Invalid navigator node type: " + node.getClass().getName()); - } DBSObjectFilter filter = new DBSObjectFilter(); if (!CommonUtils.isEmpty(include)) { filter.setInclude(include); @@ -232,11 +235,15 @@ public boolean setNavigatorNodeFilter( filter.setExclude(exclude); } filter.setEnabled(true); - ((DBNDatabaseFolder) node).setNodeFilter( - ((DBNDatabaseFolder) node).getItemsMeta(), filter, true); - if (hasNodeEditPermission(webSession, node, ((WebProjectImpl) node.getOwnerProject()).getRmProject())) { - // Save settings - ((DBNDatabaseFolder) node).getDataSourceContainer().persistConfiguration(); + if (node instanceof DBNDatabaseNode dbNode) { + DBXTreeItem itemsMeta = DBNUtils.getValidItemsMeta(webSession.getProgressMonitor(), dbNode); + dbNode.setNodeFilter(itemsMeta, filter, true); + if (node.getOwnerProject() instanceof RMControllerProvider rmControllerProvider && + hasNodeEditPermission(webSession, node, rmControllerProvider.getRMProject()) + ) { + // Save settings + dbNode.getDataSourceContainer().persistConfiguration(); + } } } catch (DBException e) { if (e instanceof DBWebException) { @@ -248,32 +255,53 @@ public boolean setNavigatorNodeFilter( } @Override - public boolean refreshNavigatorNode( + public WebNavigatorNodeInfo refreshNavigatorNode( @NotNull WebSession session, - @NotNull String nodePath + @NotNull String nodePath, + @Nullable Boolean recursive ) throws DBWebException { try { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNNode node = session.getNavigatorModel().getNodeByPath(monitor, nodePath); + DBNNode node = session.getNavigatorModelOrThrow().getNodeByPath(monitor, nodePath); if (node == null) { throw new DBWebException("Navigator node '" + nodePath + "' not found"); } - if (node instanceof DBNDataSource) { + if (node instanceof DBNDataSource dbnDataSource) { // Do not refresh entire tree - just clear child nodes // Otherwise refresh may fail if navigator settings were changed. - DBPDataSource dataSource = ((DBNDataSource) node).getDataSource(); - if (dataSource instanceof DBPRefreshableObject) { - ((DBPRefreshableObject) dataSource).refreshObject(monitor); + DBPDataSource dataSource = dbnDataSource.getDataSource(); + if (dataSource instanceof DBPRefreshableObject refreshableObject) { + // During of refreshing datasource can create new default execution context and overwrite the old one. + // It's not good for cloudbeaver because we use default execution context one for all script of the datasource + // in that way all related scripts will use the same context and overwrite the user's define context + // So why we need restore the default execution context after refreshing datasource + DBCExecutionContext defaultContext = DBUtils.getDefaultContext(refreshableObject, false); + DBCExecutionContextDefaults contextDefaults = defaultContext.getContextDefaults(); + refreshableObject.refreshObject(monitor); + if (contextDefaults != null && contextDefaults.getDefaultSchema() != null + && contextDefaults.getDefaultCatalog() != null) { + DBExecUtils.setExecutionContextDefaults( + session.getProgressMonitor(), + dataSource, + defaultContext, + contextDefaults.getDefaultCatalog().getName(), + contextDefaults.getDefaultSchema().getName(), + contextDefaults.getDefaultSchema().getName() + ); + } } - ((DBNDataSource) node).cleanupNode(); + dbnDataSource.cleanupNode(); } else if (node instanceof DBNLocalFolder) { // Refresh can't be applied to the local folder node - return true; + } else if (node instanceof DBNRoot) { + if (recursive != null && recursive) { + node.refreshNode(monitor, this); + } } else { node.refreshNode(monitor, this); } - return true; + return new WebNavigatorNodeInfo(session, node); } catch (DBException e) { throw new DBWebException("Error refreshing navigator node '" + nodePath + "'", e); } @@ -379,10 +407,9 @@ protected List getCatalogs(DBRProgressMonitor monitor, DBSO } @Nullable - protected WebNavigatorNodeInfo getNodeFromObject(WebSession session, DBSObject object){ - DBNModel navigatorModel = session.getNavigatorModel(); + protected WebNavigatorNodeInfo getNodeFromObject(WebSession session, DBSObject object) throws DBWebException { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNNode node = navigatorModel.getNodeByObject(monitor, object, false); + DBNNode node = session.getNavigatorModelOrThrow().getNodeByObject(monitor, object, false); return node == null ? null : new WebNavigatorNodeInfo(session, node); } @@ -396,7 +423,7 @@ public String renameNode( try { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNNode node = session.getNavigatorModel().getNodeByPath(monitor, nodePath); + DBNNode node = session.getNavigatorModelOrThrow().getNodeByPath(monitor, nodePath); if (node == null) { throw new DBWebException("Navigator node '" + nodePath + "' not found"); } @@ -429,32 +456,14 @@ private String renameConnectionFolder(@NotNull WebSession session, DBNNode node, List siblings = Arrays.stream( ((DBNLocalFolder) node).getLogicalParent().getChildren(session.getProgressMonitor())) .filter(n -> n instanceof DBNLocalFolder) - .map(DBNNode::getName).collect(Collectors.toList()); + .map(DBNNode::getName).toList(); if (siblings.contains(newName)) { throw new DBWebException("Name " + newName + " is unavailable or invalid"); } - var oldNodePath = node.getNodeItemPath(); node.rename(session.getProgressMonitor(), newName); - var newNodePath = node.getNodeItemPath(); - addNavigatorNodeMoveEvent(session, node, oldNodePath, newNodePath); return node.getName(); } - private void addNavigatorNodeMoveEvent(@NotNull WebSession session, DBNNode node, String oldNodePath, String newNodePath) { - WebEventUtils.addNavigatorNodeUpdatedEvent( - node.getOwnerProject(), - session, - oldNodePath, - WSConstants.EventAction.DELETE - ); - WebEventUtils.addNavigatorNodeUpdatedEvent( - node.getOwnerProject(), - session, - newNodePath, - WSConstants.EventAction.CREATE - ); - } - @NotNull private String renameRmResourceNode(@NotNull WebSession session, DBNNode node, @NotNull String newName) throws DBException { if (newName.contains("/") || newName.contains("\\")) { @@ -501,8 +510,9 @@ public int deleteNodes( String projectId = null; boolean containsFolderNodes = false; Map nodes = new LinkedHashMap<>(); + DBNModel model = session.getNavigatorModelOrThrow(); for (String path : nodePaths) { - DBNNode node = session.getNavigatorModel().getNodeByPath(monitor, path); + DBNNode node = model.getNodeByPath(monitor, path); if (node == null) { throw new DBWebException("Navigator node '" + path + "' not found"); } @@ -534,16 +544,14 @@ public int deleteNodes( DBCExecutionContext executionContext = getCommandExecutionContext(object); DBECommandContext commandContext = new WebCommandContext(executionContext, false); ne.getValue().deleteObject(commandContext, object, options); - commandContext.saveChanges(session.getProgressMonitor(), options); + try { + commandContext.saveChanges(session.getProgressMonitor(), options); + } catch (DBException e) { + commandContext.resetChanges(true); + throw e; + } } else if (node instanceof DBNLocalFolder) { - var nodePath = node.getNodeItemPath(); node.getOwnerProject().getDataSourceRegistry().removeFolder(((DBNLocalFolder) node).getFolder(), false); - WebEventUtils.addNavigatorNodeUpdatedEvent( - session.getProjectById(projectId), - session, - nodePath, - WSConstants.EventAction.DELETE - ); } else if (node instanceof DBNResourceManagerResource) { DBNResourceManagerResource rmResource = ((DBNResourceManagerResource) node); String resourceProjectId = rmResource.getResourceProject().getId(); @@ -558,7 +566,7 @@ public int deleteNodes( } } if (containsFolderNodes) { - WebServiceUtils.updateConfigAndRefreshDatabases(session, projectId); + WebServiceUtils.refreshDatabases(session, projectId); } return nodes.size(); @@ -569,7 +577,7 @@ public int deleteNodes( private void checkProjectEditAccess(DBNNode node, WebSession session) throws DBException { BaseWebProjectImpl project = (BaseWebProjectImpl) node.getOwnerProject(); - if (project == null || !hasNodeEditPermission(session, node, project.getRmProject())) { + if (project == null || !hasNodeEditPermission(session, node, project.getRMProject())) { throw new DBException("Access denied"); } } @@ -591,28 +599,32 @@ public boolean moveNodesToFolder( ) throws DBWebException { try { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNNode folderNode; - folderNode = session.getNavigatorModel().getNodeByPath(monitor, folderNodePath); + DBNModel navigatorModel = session.getNavigatorModelOrThrow(); + DBNNode folderNode = navigatorModel.getNodeByPath(monitor, folderNodePath); + if (folderNode == null) { + throw new DBException("Folder node '" + folderNodePath + "' not found"); + } for (String path : nodePaths) { - DBNNode node = session.getNavigatorModel().getNodeByPath(monitor, path); + DBNNode node = navigatorModel.getNodeByPath(monitor, path); if (node == null) { throw new DBWebException("Navigator node '" + path + "' not found"); } checkProjectEditAccess(node, session); - if (node instanceof DBNDataSource) { - DBPDataSourceFolder folder = WebConnectionFolderUtils.getParentFolder(folderNode); - ((DBNDataSource) node).moveToFolder(folderNode.getOwnerProject(), folder); - node.getOwnerProject().getDataSourceRegistry().updateDataSource( - ((DBNDataSource) node).getDataSourceContainer()); - WebEventUtils.addDataSourceUpdatedEvent( - node.getOwnerProject(), - session, - ((DBNDataSource) node).getDataSourceContainer().getId(), - WSConstants.EventAction.UPDATE, - WSDataSourceProperty.CONFIGURATION - ); - } else if (node instanceof DBNLocalFolder) { - DBPDataSourceFolder parentFolder = WebConnectionFolderUtils.getParentFolder(folderNode); + if (node.getNodeUri().equals(folderNode.getNodeUri())) { + throw new DBWebException("Cannot move node inside itself"); + } + if (node instanceof DBNDataSource dataSourceNode) { + DBPDataSourceFolder folder = null; + if (folderNode instanceof DBNLocalFolder localFolderNode) { + folder = localFolderNode.getFolder(); + } + dataSourceNode.moveToFolder(folderNode.getOwnerProject(), folder); + node.getOwnerProject().getDataSourceRegistry().updateDataSource(dataSourceNode.getDataSourceContainer()); + } else if (node instanceof DBNLocalFolder dbnLocalFolder) { + DBPDataSourceFolder parentFolder = null; + if (folderNode instanceof DBNLocalFolder parentFolderNode) { + parentFolder = parentFolderNode.getFolder(); + } if (parentFolder != null) { List siblings = Arrays.stream(parentFolder.getChildren()) .map(DBPDataSourceFolder::getName) @@ -621,15 +633,12 @@ public boolean moveNodesToFolder( throw new DBWebException("Node " + folderNodePath + " contains folder with name '" + node.getName() + "'"); } } - DBNLocalFolder dbnLocalFolder = ((DBNLocalFolder) node); - var oldNodePath = node.getNodeItemPath(); node.getOwnerProject().getDataSourceRegistry().moveFolder( dbnLocalFolder.getFolder().getFolderPath(), - dbnLocalFolder.generateNewFolderPath(parentFolder, dbnLocalFolder.getNodeName()) + dbnLocalFolder.generateNewFolderPath(parentFolder, dbnLocalFolder.getNodeDisplayName()) ); - var newNodePath = node.getNodeItemPath(); - WebServiceUtils.updateConfigAndRefreshDatabases(session, node.getOwnerProject().getId()); - addNavigatorNodeMoveEvent(session, node, oldNodePath, newNodePath); + node.getOwnerProject().getDataSourceRegistry().checkForErrors(); + WebServiceUtils.refreshDatabases(session, node.getOwnerProject().getId()); } else if (node instanceof DBNResourceManagerResource) { boolean rmNewNode = folderNode instanceof DBNAbstractResourceManagerNode; DBNResourceManagerResource rmOldNode = (DBNResourceManagerResource) node; @@ -677,7 +686,7 @@ private String renameDatabaseObject(WebSession session, DBNDatabaseNode node, St } } } - throw new DBException("Node " + node.getNodeItemPath() + " rename is not supported"); + throw new DBException("Node " + node.getNodeUri() + " rename is not supported"); } public DBCExecutionContext getCommandExecutionContext(DBSObject object) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java deleted file mode 100644 index a550efc091..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * 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.session; - -import io.cloudbeaver.DBWebException; -import io.cloudbeaver.auth.SMTokenCredentialProvider; -import io.cloudbeaver.model.session.BaseWebSession; -import io.cloudbeaver.model.session.WebHeadlessSession; -import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.model.session.WebSessionAuthProcessor; -import io.cloudbeaver.registry.WebHandlerRegistry; -import io.cloudbeaver.registry.WebSessionHandlerDescriptor; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.service.DBWSessionHandler; -import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.auth.SMAuthInfo; -import org.jkiss.dbeaver.model.security.user.SMAuthPermissions; -import org.jkiss.dbeaver.model.websocket.WSConstants; -import org.jkiss.dbeaver.model.websocket.event.session.WSSessionStateEvent; -import org.jkiss.utils.CommonUtils; - -import java.util.*; -import java.util.stream.Collectors; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -/** - * Web session manager - */ -public class WebSessionManager { - - private static final Log log = Log.getLog(WebSessionManager.class); - - private final CBApplication application; - private final Map sessionMap = new HashMap<>(); - - public WebSessionManager(CBApplication application) { - this.application = application; - } - - /** - * Closes Web Session, associated to HttpSession from {@code request} - */ - public BaseWebSession closeSession(@NotNull HttpServletRequest request) { - HttpSession session = request.getSession(); - if (session != null) { - BaseWebSession webSession; - synchronized (sessionMap) { - webSession = sessionMap.remove(session.getId()); - } - if (webSession != null) { - log.debug("> Close session '" + session.getId() + "'"); - webSession.close(); - return webSession; - } - } - return null; - } - - protected CBApplication getApplication() { - return application; - } - - public boolean touchSession(@NotNull HttpServletRequest request, - @NotNull HttpServletResponse response) throws DBWebException { - WebSession webSession = getWebSession(request, response, false); - webSession.updateInfo(request, response); - return true; - } - - @NotNull - public WebSession getWebSession(@NotNull HttpServletRequest request, - @NotNull HttpServletResponse response) throws DBWebException { - return getWebSession(request, response, true); - } - - @NotNull - public WebSession getWebSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, boolean errorOnNoFound) throws DBWebException { - return getWebSession(request, response, true, errorOnNoFound); - } - - @NotNull - public WebSession getWebSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, boolean updateInfo, boolean errorOnNoFound) throws DBWebException { - HttpSession httpSession = request.getSession(true); - String sessionId = httpSession.getId(); - WebSession webSession; - synchronized (sessionMap) { - var baseWebSession = sessionMap.get(sessionId); - if (baseWebSession == null && CBApplication.getInstance().isConfigurationMode()) { - try { - webSession = createWebSessionImpl(httpSession); - } catch (DBException e) { - throw new DBWebException("Failed to create web session", e); - } - sessionMap.put(sessionId, webSession); - } else if (baseWebSession == null) { - try { - webSession = createWebSessionImpl(httpSession); - } catch (DBException e) { - throw new DBWebException("Failed to create web session", e); - } - - boolean restored = false; - try { - restored = restorePreviousUserSession(webSession); - } catch (DBException e) { - log.error("Failed to restore previous user session", e); - } - - if (!restored && errorOnNoFound && !httpSession.isNew()) { - throw new DBWebException("Session has expired", DBWebException.ERROR_CODE_SESSION_EXPIRED); - } - - log.debug((restored ? "Restored " : "New ") + "web session '" + webSession.getSessionId() + "'"); - - webSession.setCacheExpired(!httpSession.isNew()); - - sessionMap.put(sessionId, webSession); - } else { - if (!(baseWebSession instanceof WebSession)) { - throw new DBWebException("Unexpected session type: " + baseWebSession.getClass().getName()); - } - webSession = (WebSession) baseWebSession; - if (updateInfo) { - // Update only once per request - if (!CommonUtils.toBoolean(request.getAttribute("sessionUpdated"))) { - webSession.updateInfo(request, response); - request.setAttribute("sessionUpdated", true); - } - } - } - } - - return webSession; - } - - /** - * Returns not expired session from cache, or restore it. - * - * @return WebSession object or null, if session expired or invalid - */ - @Nullable - public WebSession getOrRestoreSession(@NotNull HttpServletRequest request) { - var httpSession = request.getSession(); - if (httpSession == null) { - log.debug("Http session is null. No Web Session returned"); - return null; - } - var sessionId = httpSession.getId(); - WebSession webSession; - synchronized (sessionMap) { - if (sessionMap.containsKey(sessionId)) { - var cachedWebSession = sessionMap.get(sessionId); - if (!(cachedWebSession instanceof WebSession)) { - log.warn("Unexpected session type: " + cachedWebSession.getClass().getName()); - return null; - } - return (WebSession) cachedWebSession; - } else { - try { - var oldAuthInfo = getApplication().getSecurityController().restoreUserSession(sessionId); - if (oldAuthInfo == null) { - log.debug("Couldn't restore previous user session '" + sessionId + "'"); - return null; - } - - webSession = createWebSessionImpl(httpSession); - restorePreviousUserSession(webSession, oldAuthInfo); - - sessionMap.put(sessionId, webSession); - log.debug("Web session restored"); - return webSession; - } catch (DBException e) { - log.error("Failed to restore previous user session", e); - return null; - } - } - } - } - - private boolean restorePreviousUserSession(@NotNull WebSession webSession) throws DBException { - var oldAuthInfo = webSession.getSecurityController().restoreUserSession(webSession.getSessionId()); - if (oldAuthInfo == null) { - return false; - } - - restorePreviousUserSession(webSession, oldAuthInfo); - return true; - } - - private void restorePreviousUserSession( - @NotNull WebSession webSession, - @NotNull SMAuthInfo authInfo - ) throws DBException { - var linkWithActiveUser = false; // because its old credentials and should already be linked if needed - new WebSessionAuthProcessor(webSession, authInfo, linkWithActiveUser) - .authenticateSession(); - } - - @NotNull - protected WebSession createWebSessionImpl(@NotNull HttpSession httpSession) throws DBException { - return new WebSession(httpSession, application, getSessionHandlers()); - } - - @NotNull - protected Map getSessionHandlers() { - return WebHandlerRegistry.getInstance().getSessionHandlers() - .stream() - .collect(Collectors.toMap(WebSessionHandlerDescriptor::getId, WebSessionHandlerDescriptor::getInstance)); - } - - @Nullable - public BaseWebSession getSession(@NotNull String sessionId) { - synchronized (sessionMap) { - return sessionMap.get(sessionId); - } - } - - @Nullable - public WebSession findWebSession(HttpServletRequest request) { - String sessionId = request.getSession().getId(); - synchronized (sessionMap) { - var session = sessionMap.get(sessionId); - if (session instanceof WebSession) { - return (WebSession) session; - } - return null; - } - } - - public WebSession findWebSession(HttpServletRequest request, boolean errorOnNoFound) throws DBWebException { - WebSession webSession = findWebSession(request); - if (webSession != null) { - return webSession; - } - if (errorOnNoFound) { - throw new DBWebException("Session has expired", DBWebException.ERROR_CODE_SESSION_EXPIRED); - } - return null; - } - - public void expireIdleSessions() { - long maxSessionIdleTime = application.getMaxSessionIdleTime(); - - List expiredList = new ArrayList<>(); - synchronized (sessionMap) { - for (Iterator iterator = sessionMap.values().iterator(); iterator.hasNext(); ) { - var session = iterator.next(); - long idleMillis = System.currentTimeMillis() - session.getLastAccessTimeMillis(); - if (idleMillis >= maxSessionIdleTime) { - iterator.remove(); - expiredList.add(session); - } - } - } - - for (var session : expiredList) { - log.debug("> Expire session '" + session.getSessionId() + "'"); - session.close(); - } - } - - public Collection getAllActiveSessions() { - synchronized (sessionMap) { - return sessionMap.values(); - } - } - - @Nullable - public WebHeadlessSession getHeadlessSession(HttpServletRequest request, boolean create) throws DBException { - String smAccessToken = request.getHeader(WSConstants.WS_AUTH_HEADER); - if (CommonUtils.isEmpty(smAccessToken)) { - return null; - } - synchronized (sessionMap) { - var httpSession = request.getSession(); - var tempCredProvider = new SMTokenCredentialProvider(smAccessToken); - SMAuthPermissions authPermissions = application.createSecurityController(tempCredProvider).getTokenPermissions(); - var sessionId = httpSession != null ? httpSession.getId() - : authPermissions.getSessionId(); - - var existSession = sessionMap.get(sessionId); - - if (existSession instanceof WebHeadlessSession) { - return (WebHeadlessSession) existSession; - } - if (existSession != null) { - //session exist but it not headless session - return null; - } - if (!create) { - return null; - } - var headlessSession = new WebHeadlessSession( - sessionId, - application - ); - headlessSession.getUserContext().refresh( - smAccessToken, - null, - authPermissions - ); - sessionMap.put(sessionId, headlessSession); - return headlessSession; - } - } - - /** - * Send session state with remaining alive time to all cached session - */ - public void sendSessionsStates() { - synchronized (sessionMap) { - for (var session : sessionMap.values()) { - if (session instanceof WebHeadlessSession) { - continue; - } - try { - session.addSessionEvent(new WSSessionStateEvent(session.getRemainingTime(), session.isValid())); - } catch (Exception e) { - log.error("Failed to refresh session state: " + session.getSessionId(), 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..420f51b1b2 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,16 +16,20 @@ */ package io.cloudbeaver.service.sql; -import io.cloudbeaver.service.DBWService; +import io.cloudbeaver.DBWConstants; import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebAction; +import io.cloudbeaver.WebObjectId; import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.WebConnectionInfo; +import io.cloudbeaver.model.WebTransactionLogInfo; 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; import org.jkiss.dbeaver.model.exec.DBCLogicalOperator; +import org.jkiss.dbeaver.model.exec.trace.DBCTraceProperty; import org.jkiss.dbeaver.model.sql.registry.SQLGeneratorDescriptor; import java.util.List; @@ -88,15 +92,17 @@ WebSQLContextInfo createContext( @WebAction void setContextDefaults(@NotNull WebSQLContextInfo sqlContext, String catalogName, String schemaName) throws DBWebException; - @WebAction + @WebAction(requireGlobalPermissions = DBWConstants.GLOBAL_PERMISSION_SCRIPT_EXECUTE) WebAsyncTaskInfo asyncExecuteQuery( + @NotNull WebSession webSession, + @WebObjectId @NotNull String projectId, @NotNull WebSQLContextInfo contextInfo, @NotNull String sql, @Nullable String resultId, @Nullable WebSQLDataFilter filter, @Nullable WebDataFormat dataFormat, - boolean readLogs, - @NotNull WebSession webSession) throws DBException; + boolean readLogs + ) throws DBException; @WebAction WebAsyncTaskInfo asyncReadDataFromContainer( @@ -106,25 +112,67 @@ WebAsyncTaskInfo asyncReadDataFromContainer( @Nullable WebSQLDataFilter filter, @Nullable WebDataFormat dataFormat) throws DBWebException; + /** + * Reads dynamic trace from provided database results. + */ + @NotNull @WebAction - Boolean closeResult(@NotNull WebSQLContextInfo sqlContext, @NotNull String resultId) throws DBWebException; + List readDynamicTrace( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId + ) throws DBException; @WebAction + Boolean closeResult(@NotNull WebSQLContextInfo sqlContext, @NotNull String resultId) throws DBWebException; + + /** + * Updates result set data (sync function). + */ + @WebAction(requireGlobalPermissions = DBWConstants.GLOBAL_PERMISSION_DATA_EDITOR_EDITING) + @Deprecated // use async function WebSQLExecuteInfo updateResultsDataBatch( @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, @Nullable List updatedRows, @Nullable List deletedRows, - @Nullable List addedRows, WebDataFormat dataFormat) throws DBWebException; + @Nullable List addedRows, + @Nullable WebDataFormat dataFormat + ) throws DBWebException; + + /** + * Creates async task for updating results data. + */ + @WebAction(requireGlobalPermissions = DBWConstants.GLOBAL_PERMISSION_DATA_EDITOR_EDITING) + WebAsyncTaskInfo asyncUpdateResultsDataBatch( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @Nullable List updatedRows, + @Nullable List deletedRows, + @Nullable List addedRows, + @Nullable WebDataFormat dataFormat + ) throws DBWebException; + /** + * Reads cell LOB value from result set. + */ @WebAction String readLobValue( - @NotNull WebSQLContextInfo contextInfo, - @NotNull String resultsId, - @NotNull Integer lobColumnIndex, - @Nullable List row) throws DBWebException; + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @NotNull Integer lobColumnIndex, + @NotNull WebSQLResultsRow row) throws DBWebException; + @NotNull @WebAction + String getCellValue( + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @NotNull Integer lobColumnIndex, + @NotNull WebSQLResultsRow row) throws DBWebException; + + @WebAction(requireGlobalPermissions = DBWConstants.GLOBAL_PERMISSION_DATA_EDITOR_EDITING) String updateResultsDataBatchScript( @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, @@ -157,4 +205,35 @@ String generateGroupByQuery(@NotNull WebSQLContextInfo contextInfo, @NotNull List columnsList, @Nullable List functions, @Nullable Boolean showDuplicatesOnly) throws DBWebException; + + @WebAction + WebAsyncTaskInfo getRowDataCount(@NotNull WebSession webSession, @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId) throws DBWebException; + + @Nullable + @WebAction + Long getRowDataCountResult(@NotNull WebSession webSession, @NotNull String taskId) throws DBWebException; + + @WebAction + WebAsyncTaskInfo asyncSqlSetAutoCommit( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo contextInfo, + boolean autoCommit + ) throws DBWebException; + + @WebAction + WebAsyncTaskInfo asyncSqlRollbackTransaction( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo contextInfo + ) throws DBWebException; + + @WebAction + WebAsyncTaskInfo asyncSqlCommitTransaction( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo sqlContext); + + @WebAction + WebTransactionLogInfo getTransactionLogInfo( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo sqlContext + ); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebExecutionSource.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebExecutionSource.java index d5f6f6f55d..32dd4ad56b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebExecutionSource.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebExecutionSource.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCellValueReceiver.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCellValueReceiver.java new file mode 100644 index 0000000000..65fa368a5d --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCellValueReceiver.java @@ -0,0 +1,85 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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 org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.data.DBDAttributeBindingMeta; +import org.jkiss.dbeaver.model.data.DBDContent; +import org.jkiss.dbeaver.model.data.DBDDataReceiver; +import org.jkiss.dbeaver.model.data.DBDValue; +import org.jkiss.dbeaver.model.exec.*; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.struct.DBSDataContainer; +import org.jkiss.dbeaver.utils.ContentUtils; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class WebSQLCellValueReceiver implements DBDDataReceiver { + protected final DBSDataContainer dataContainer; + protected int rowIndex; + protected DBDAttributeBindingMeta binding; + protected Object value; + + public WebSQLCellValueReceiver(DBSDataContainer dataContainer, int rowIndex) { + this.dataContainer = dataContainer; + this.rowIndex = rowIndex; + } + + @Override + public void fetchStart(@NotNull DBCSession session, @NotNull DBCResultSet resultSet, long offset, long maxRows) throws DBCException { + DBCResultSetMetaData meta = resultSet.getMeta(); + List attributes = meta.getAttributes(); + DBCAttributeMetaData attrMeta = attributes.get(rowIndex); + binding = new DBDAttributeBindingMeta(dataContainer, resultSet.getSession(), attrMeta); + } + + @Override + public void fetchRow(@NotNull DBCSession session, @NotNull DBCResultSet resultSet) throws DBCException { + value = binding.getValueHandler().fetchValueObject( + resultSet.getSession(), + resultSet, + binding.getMetaAttribute(), + rowIndex); + } + + @Override + public void fetchEnd(@NotNull DBCSession session, @NotNull DBCResultSet resultSet) throws DBCException { + + } + + @Override + public void close() { + + } + + @NotNull + public byte[] getBinaryValue(DBRProgressMonitor monitor) throws DBCException { + byte[] binaryValue; + if (value instanceof DBDContent dbdContent) { + binaryValue = ContentUtils.getContentBinaryValue(monitor, dbdContent); + } else if (value instanceof DBDValue dbdValue) { + binaryValue = dbdValue.getRawValue().toString().getBytes(); + } else { + binaryValue = value.toString().getBytes(StandardCharsets.UTF_8); + } + if (binaryValue == null) { + throw new DBCException("Lob value is null"); + } + return binaryValue; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java index 332473bd56..5a303d37e5 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -21,6 +21,7 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.*; import org.jkiss.dbeaver.model.exec.DBCExecutionContext; +import org.jkiss.dbeaver.model.sql.SQLModelPreferences; import org.jkiss.dbeaver.model.sql.SQLSyntaxManager; import org.jkiss.dbeaver.model.sql.completion.SQLCompletionContext; import org.jkiss.dbeaver.model.sql.completion.SQLCompletionProposalBase; @@ -66,7 +67,8 @@ public SQLRuleManager getRuleManager() { @Override public boolean isUseFQNames() { - return false; + return sqlContext.getWebSession().getUserPreferenceStore() + .getBoolean(SQLModelPreferences.SQL_EDITOR_PROPOSAL_ALWAYS_FQ); } @Override @@ -101,7 +103,7 @@ public boolean isSearchInsideNames() { @Override public boolean isSortAlphabetically() { - return false; + return true; } @Override @@ -119,8 +121,13 @@ public boolean isShowValues() { return true; } + @Override + public boolean isForceQualifiedColumnNames() { + return false; + } + @Override public SQLCompletionProposalBase createProposal(@NotNull SQLCompletionRequest request, @NotNull String displayString, @NotNull String replacementString, int cursorPosition, @Nullable DBPImage image, @NotNull DBPKeywordType proposalType, @Nullable String description, @Nullable DBPNamedObject object, @NotNull Map params) { - return new SQLCompletionProposalBase(this, request.getWordDetector(), displayString, replacementString, cursorPosition, image, proposalType, description, object, params); + return new SQLCompletionProposalBase(request, displayString, replacementString, cursorPosition, image, proposalType, description, object, params); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContextScriptParser.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContextScriptParser.java new file mode 100644 index 0000000000..0fa553a5fe --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContextScriptParser.java @@ -0,0 +1,92 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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 io.cloudbeaver.model.session.WebSession; +import org.eclipse.jface.text.Document; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.sql.SQLModelPreferences; +import org.jkiss.dbeaver.model.sql.completion.SQLCompletionRequest; +import org.jkiss.dbeaver.model.sql.parser.SQLParserContext; +import org.jkiss.dbeaver.model.sql.parser.SQLScriptParser; +import org.jkiss.dbeaver.model.sql.semantics.SQLDocumentSyntaxContext; +import org.jkiss.dbeaver.model.sql.semantics.SQLQueryModelRecognizer; +import org.jkiss.dbeaver.model.sql.semantics.SQLQueryRecognitionContext; +import org.jkiss.dbeaver.model.sql.semantics.SQLScriptItemAtOffset; +import org.jkiss.dbeaver.model.sql.semantics.completion.SQLQueryCompletionContext; + +public class WebSQLCompletionContextScriptParser { + + @NotNull + public static SQLQueryCompletionContext obtainCompletionContext( + WebSession webSession, + @NotNull String query, + int position, + SQLCompletionRequest request + ) { + Document document = new Document(query); + SQLDocumentSyntaxContext syntaxContext = new SQLDocumentSyntaxContext(); + SQLParserContext parserContext = new SQLParserContext( + request.getContext().getDataSource(), + request.getContext().getSyntaxManager(), + request.getContext().getRuleManager(), + document + ); + var scriptItems = SQLScriptParser.parseScript( + parserContext.getDataSource(), + parserContext.getDialect(), + parserContext.getPreferenceStore(), + document.get() + ); + if (scriptItems != null) { + for (var item : scriptItems) { + var model = SQLQueryModelRecognizer.recognizeQuery( + new SQLQueryRecognitionContext( + webSession.getProgressMonitor(), + request.getContext().getExecutionContext(), + true, + webSession.getUserPreferenceStore().getBoolean(SQLModelPreferences.VALIDATE_FUNCTIONS), + request.getContext().getSyntaxManager(), + request.getContext().getDataSource().getSQLDialect() + ), + item.getOriginalText() + ); + syntaxContext.registerScriptItemContext( + item.getOriginalText(), + model, + item.getOffset(), + item.getLength(), + true + ); + } + } + + SQLScriptItemAtOffset scriptItem = syntaxContext.findScriptItem(position); + if (scriptItem != null) { + scriptItem.item.setHasContextBoundaryAtLength(false); + return SQLQueryCompletionContext.prepareCompletionContext( + scriptItem, + position, + request.getContext().getExecutionContext(), + request.getContext().getDataSource().getSQLDialect() + ); + } else { + return SQLQueryCompletionContext.prepareOffquery(0, position); + } + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionProposal.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionProposal.java index 4a4be0d114..fc18d8240c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionProposal.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionProposal.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -18,7 +18,8 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPImage; -import org.jkiss.dbeaver.model.sql.completion.SQLCompletionProposalBase; +import org.jkiss.dbeaver.model.DBPKeywordType; +import org.jkiss.dbeaver.model.sql.completion.CompletionProposalBase; /** * Web SQL dialect. @@ -27,9 +28,9 @@ public class WebSQLCompletionProposal { private static final Log log = Log.getLog(WebSQLCompletionProposal.class); - private SQLCompletionProposalBase proposal; + private CompletionProposalBase proposal; - public WebSQLCompletionProposal(SQLCompletionProposalBase proposal) { + public WebSQLCompletionProposal(CompletionProposalBase proposal) { this.proposal = proposal; } @@ -38,7 +39,7 @@ public String getDisplayString() { } public String getType() { - return proposal.getProposalType().name(); + return proposal.getProposalType() != null ? proposal.getProposalType().name() : DBPKeywordType.OTHER.name(); } public String getReplacementString() { @@ -66,4 +67,8 @@ public String getNodePath() { return null; } + public CompletionProposalBase getProposal() { + return proposal; + } + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java index 15b7b2c2a7..8fe043ddc0 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -19,21 +19,43 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebAction; import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.model.WebAsyncTaskInfo; +import io.cloudbeaver.model.WebTransactionLogInfo; +import io.cloudbeaver.model.WebTransactionLogItemInfo; +import io.cloudbeaver.model.session.WebAsyncTaskProcessor; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebSessionProvider; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBConstants; +import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.data.DBDAttributeBinding; -import org.jkiss.dbeaver.model.exec.DBCException; -import org.jkiss.dbeaver.model.exec.DBCExecutionContextDefaults; -import org.jkiss.dbeaver.model.exec.DBExecUtils; +import org.jkiss.dbeaver.model.exec.*; +import org.jkiss.dbeaver.model.exec.trace.DBCTrace; +import org.jkiss.dbeaver.model.messages.ModelMessages; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.qm.QMTransactionState; +import org.jkiss.dbeaver.model.qm.QMUtils; +import org.jkiss.dbeaver.model.qm.meta.QMMConnectionInfo; +import org.jkiss.dbeaver.model.qm.meta.QMMStatementExecuteInfo; +import org.jkiss.dbeaver.model.qm.meta.QMMTransactionInfo; +import org.jkiss.dbeaver.model.qm.meta.QMMTransactionSavepointInfo; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.struct.DBSDataContainer; import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog; import org.jkiss.dbeaver.model.struct.rdb.DBSSchema; +import org.jkiss.dbeaver.model.websocket.event.WSTransactionalCountEvent; +import org.jkiss.dbeaver.utils.RuntimeUtils; import org.jkiss.utils.CommonUtils; +import java.lang.reflect.InvocationTargetException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -44,13 +66,16 @@ public class WebSQLContextInfo implements WebSessionProvider { private static final Log log = Log.getLog(WebSQLContextInfo.class); - private final WebSQLProcessor processor; + private final transient WebSQLProcessor processor; private final String id; private final String projectId; private final Map resultInfoMap = new HashMap<>(); private final AtomicInteger resultId = new AtomicInteger(); + public static final DateTimeFormatter ISO_DATE_FORMAT = DateTimeFormatter.ofPattern(DBConstants.DEFAULT_ISO_TIMESTAMP_FORMAT) + .withZone(ZoneId.of("UTC")); + public WebSQLContextInfo( WebSQLProcessor processor, String id, String catalogName, String schemaName, String projectId ) throws DBCException { @@ -135,13 +160,23 @@ public void setDefaults(String catalogName, String schemaName) throws DBWebExcep } } + /** + * Saves results info into cache. + * Helps to find it with results id sent by front-end. + */ @NotNull - public WebSQLResultsInfo saveResult(@NotNull DBSDataContainer dataContainer, @NotNull DBDAttributeBinding[] attributes) { + public WebSQLResultsInfo saveResult( + @NotNull DBSDataContainer dataContainer, + @NotNull DBCTrace trace, + @NotNull DBDAttributeBinding[] attributes, + boolean singleRow) { WebSQLResultsInfo resultInfo = new WebSQLResultsInfo( dataContainer, String.valueOf(resultId.incrementAndGet()) ); resultInfo.setAttributes(attributes); + resultInfo.setSingleRow(singleRow); + resultInfo.setTrace(trace); resultInfoMap.put(resultInfo.getId(), resultInfo); return resultInfo; } @@ -170,4 +205,188 @@ void dispose() { public WebSession getWebSession() { return processor.getWebSession(); } + + + /////////////////////////////////////////////////////// + // Transactions + + public WebAsyncTaskInfo setAutoCommit(boolean autoCommit) { + DBCExecutionContext context = processor.getExecutionContext(); + DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + if (txnManager != null) { + monitor.beginTask("Change connection auto-commit to " + autoCommit, 1); + try { + monitor.subTask("Change context '" + context.getContextName() + "' auto-commit state"); + txnManager.setAutoCommit(monitor, autoCommit); + result = true; + } catch (DBException e) { + throw new InvocationTargetException(e); + } finally { + monitor.done(); + } + } + + } + }; + return getWebSession().createAndRunAsyncTask("Set auto-commit", runnable); + + } + + public WebTransactionLogInfo getTransactionLogInfo() { + DBCExecutionContext context = processor.getExecutionContext(); + return getTransactionLogInfo(context); + } + + @NotNull + private WebTransactionLogInfo getTransactionLogInfo(DBCExecutionContext executionContext) { + int updateCount = 0; + List logItemInfos = new ArrayList<>(); + QMMConnectionInfo sessionInfo = QMUtils.getCurrentConnection(executionContext); + if (sessionInfo.isTransactional()) { + QMMTransactionInfo txnInfo = sessionInfo.getTransaction(); + if (txnInfo != null) { + QMMTransactionSavepointInfo sp = txnInfo.getCurrentSavepoint(); + QMMStatementExecuteInfo execInfo = sp.getLastExecute(); + for (QMMStatementExecuteInfo exec = execInfo; exec != null && exec.getSavepoint() == sp; exec = exec.getPrevious()) { + if (exec.getUpdateRowCount() > 0 ) { + DBCExecutionPurpose purpose = exec.getStatement().getPurpose(); + if (!exec.hasError() && purpose != DBCExecutionPurpose.META && purpose != DBCExecutionPurpose.UTIL) { + updateCount++; + } + generateLogInfo(logItemInfos, exec, purpose, updateCount); + } + } + } + } else { + QMMStatementExecuteInfo execInfo = sessionInfo.getExecutionStack(); + for (QMMStatementExecuteInfo exec = execInfo; exec != null; exec = exec.getPrevious()) { + if (exec.getUpdateRowCount() > 0) { + updateCount++; + DBCExecutionPurpose purpose = exec.getStatement().getPurpose(); + generateLogInfo(logItemInfos, exec, purpose, updateCount); + } + } + } + return new WebTransactionLogInfo(logItemInfos, updateCount); + } + + private void generateLogInfo( + @NotNull List logItemInfos, + @NotNull QMMStatementExecuteInfo exec, + @NotNull DBCExecutionPurpose purpose, + int id + ) { + String type = "SQL / " + purpose.getTitle(); + String dateTime = ISO_DATE_FORMAT.format(Instant.ofEpochMilli(exec.getCloseTime())); + String result = ModelMessages.controls_querylog_success; + if (exec.hasError()) { + if (exec.getErrorCode() == 0) { + result = exec.getErrorMessage(); + } else if (exec.getErrorMessage() == null) { + result = ModelMessages.controls_querylog_error + exec.getErrorCode() + "]"; //$NON-NLS-1$ + } else { + result = "[" + exec.getErrorCode() + "] " + exec.getErrorMessage(); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + logItemInfos.add( + new WebTransactionLogItemInfo(id, dateTime, type, exec.getQueryString(), + exec.getDuration(), exec.getUpdateRowCount(), result) + ); + } + + + public WebAsyncTaskInfo commitTransaction() { + DBCExecutionContext context = processor.getExecutionContext(); + DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + if (txnManager != null) { + QMTransactionState txnInfo = QMUtils.getTransactionState(context); + try (DBCSession session = context.openSession(monitor, DBCExecutionPurpose.UTIL, "Commit transaction")) { + txnManager.commit(session); + } catch (DBCException e) { + throw new InvocationTargetException(e); + } + result = """ + Transaction has been committed + Query count: %s + Duration: %s + """.formatted( + txnInfo.getUpdateCount(), + RuntimeUtils.formatExecutionTime(System.currentTimeMillis() - txnInfo.getTransactionStartTime()) + ); + } + processor.getWebSession().addSessionEvent( + new WSTransactionalCountEvent( + processor.getWebSession().getSessionId(), + processor.getWebSession().getUserId(), + getProjectId(), + getId(), + getConnectionId(), + 0 + ) + ); + + } + }; + return getWebSession().createAndRunAsyncTask("Commit transaction", runnable); + } + + + public WebAsyncTaskInfo rollbackTransaction() { + DBCExecutionContext context = processor.getExecutionContext(); + DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + if (txnManager != null) { + QMTransactionState txnInfo = QMUtils.getTransactionState(context); + try (DBCSession session = context.openSession(monitor, DBCExecutionPurpose.UTIL, "Rollback transaction")) { + txnManager.rollback(session, null); + } catch (DBCException e) { + throw new InvocationTargetException(e); + } + result = """ + Transaction has been rolled back + Query count: %s + Duration: %s + """.formatted( + txnInfo.getUpdateCount(), + RuntimeUtils.formatExecutionTime(System.currentTimeMillis() - txnInfo.getTransactionStartTime()) + ); + processor.getWebSession().addSessionEvent( + new WSTransactionalCountEvent( + processor.getWebSession().getSessionId(), + processor.getWebSession().getUserId(), + getProjectId(), + getId(), + getConnectionId(), + 0 + ) + ); + } + } + }; + + return getWebSession().createAndRunAsyncTask("Rollback transaction", runnable); + } + + @Property + public Boolean isAutoCommit() throws DBWebException { + DBCExecutionContext context = processor.getExecutionContext(); + DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); + if (txnManager == null || !txnManager.isSupportsTransactions()) { + return null; + } + try { + return txnManager.isAutoCommit(); + } catch (DBException e) { + throw new DBWebException("Error getting auto-commit parameter from context", e); + } + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataFilter.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataFilter.java index 724ad2c846..30d9b5d424 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataFilter.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataFilter.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java index 8284290fed..62c718a3c9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,43 +16,31 @@ */ package io.cloudbeaver.service.sql; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBConstants; -import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.server.WebAppUtils; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.data.*; -import org.jkiss.dbeaver.model.exec.*; +import org.jkiss.dbeaver.model.exec.DBCException; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; -import org.jkiss.dbeaver.model.sql.DBQuotaException; import org.jkiss.dbeaver.model.struct.DBSDataContainer; -import org.jkiss.dbeaver.utils.ContentUtils; import org.jkiss.utils.CommonUtils; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.sql.Timestamp; import java.text.SimpleDateFormat; -import java.util.List; -public class WebSQLDataLOBReceiver implements DBDDataReceiver { +public class WebSQLDataLOBReceiver extends WebSQLCellValueReceiver { private static final Log log = Log.getLog(WebSQLDataLOBReceiver.class); - public static final Path DATA_EXPORT_FOLDER = CBPlatform.getInstance().getTempFolder(new VoidProgressMonitor(), "sql-lob-files"); - + public static final Path DATA_EXPORT_FOLDER = WebAppUtils.getWebPlatform().getTempFolder(new VoidProgressMonitor(), "sql-lob" + + "-files"); private final String tableName; - private final DBSDataContainer dataContainer; - - private DBDAttributeBinding binding; - private Object lobValue; - private int rowIndex; WebSQLDataLOBReceiver(String tableName, DBSDataContainer dataContainer, int rowIndex) { + super(dataContainer, rowIndex); this.tableName = tableName; - this.dataContainer = dataContainer; - this.rowIndex = rowIndex; - if (!Files.exists(DATA_EXPORT_FOLDER)){ + if (!Files.exists(DATA_EXPORT_FOLDER)) { try { Files.createDirectories(DATA_EXPORT_FOLDER); } catch (IOException e) { @@ -62,62 +50,21 @@ public class WebSQLDataLOBReceiver implements DBDDataReceiver { } - public String createLobFile(DBCSession session) throws DBCException, IOException { + public String createLobFile(DBRProgressMonitor monitor) throws DBCException, IOException { String exportFileName = CommonUtils.truncateString(tableName, 32); StringBuilder fileName = new StringBuilder(exportFileName); fileName.append("_") - .append(binding.getName()) - .append("_"); + .append(binding.getName()) + .append("_"); Timestamp ts = new Timestamp(System.currentTimeMillis()); String s = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(ts); fileName.append(s); exportFileName = CommonUtils.escapeFileName(fileName.toString()); - byte[] binaryValue; - Number fileSizeLimit = CBApplication.getInstance().getAppConfiguration().getResourceQuota(CBConstants.QUOTA_PROP_FILE_LIMIT); - if (lobValue instanceof DBDContent) { - binaryValue = ContentUtils.getContentBinaryValue(session.getProgressMonitor(), (DBDContent) lobValue); - } else if (lobValue instanceof DBDValue) { - binaryValue = ((DBDValue) lobValue).getRawValue().toString().getBytes(); - } else { - binaryValue = lobValue.toString().getBytes(StandardCharsets.UTF_8); - } - if (binaryValue == null) { - throw new DBCException("Lob value is null"); - } - if (binaryValue.length > fileSizeLimit.longValue()) { - throw new DBQuotaException( - "Data export quota exceeded", CBConstants.QUOTA_PROP_FILE_LIMIT, fileSizeLimit.longValue(), binaryValue.length); - } - Path file = DATA_EXPORT_FOLDER.resolve(exportFileName); + byte[] binaryValue = getBinaryValue(monitor); + Path file = WebSQLDataLOBReceiver.DATA_EXPORT_FOLDER.resolve(exportFileName); Files.write(file, binaryValue); return exportFileName; } - - @Override - public void fetchStart(DBCSession session, DBCResultSet resultSet, long offset, long maxRows) throws DBCException { - DBCResultSetMetaData meta = resultSet.getMeta(); - List attributes = meta.getAttributes(); - DBCAttributeMetaData attrMeta = attributes.get(rowIndex); - binding = new DBDAttributeBindingMeta(dataContainer, resultSet.getSession(), attrMeta); - } - @Override - public void fetchRow(DBCSession session, DBCResultSet resultSet) throws DBCException { - lobValue = binding.getValueHandler().fetchValueObject( - resultSet.getSession(), - resultSet, - binding.getMetaAttribute(), - rowIndex); - } - - @Override - public void fetchEnd(DBCSession session, DBCResultSet resultSet) throws DBCException { - - } - - @Override - public void close() { - - } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDatabaseDocument.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDatabaseDocument.java index f49613a421..402f7406e2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDatabaseDocument.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDatabaseDocument.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -22,8 +22,7 @@ import org.jkiss.dbeaver.model.data.DBDDocument; import org.jkiss.dbeaver.model.exec.DBCException; -import java.io.ByteArrayOutputStream; -import java.nio.charset.StandardCharsets; +import java.io.StringWriter; import java.util.Collections; import java.util.Map; @@ -63,9 +62,9 @@ public String getData() throws DBCException { return null; } try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.serializeDocument(webSession.getProgressMonitor(), baos, StandardCharsets.UTF_8); - return new String(baos.toByteArray(), StandardCharsets.UTF_8); + StringWriter writer = new StringWriter(); + document.serializeDocument(webSession.getProgressMonitor(), writer); + return writer.toString(); } catch (Exception e) { throw new DBCException("Error serializing document", e); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDialectInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDialectInfo.java index 7f2bd65ba8..94bf8d398e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDialectInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDialectInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecuteInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecuteInfo.java index 99c13487ed..c90d0681e1 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecuteInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecuteInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecutionPlan.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecutionPlan.java index baa8e4269a..b1eba34512 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecutionPlan.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecutionPlan.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecutionPlanNode.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecutionPlanNode.java index 391702a0b0..c322cfb009 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecutionPlanNode.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLExecutionPlanNode.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. 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..cced78d2d7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java @@ -0,0 +1,108 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.app.ServletApplication; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.WebAppUtils; +import io.cloudbeaver.service.WebServiceServletBase; +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.MultipartConfig; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +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 java.util.UUID; + +@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(ServletApplication 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 = WebAppUtils.getWebPlatform() + .getTempFolder(session.getProgressMonitor(), TEMP_FILE_FOLDER) + .resolve(session.getSessionId()); + + MultipartConfigElement multiPartConfig = new MultipartConfigElement(tempFolder.toString()); + request.setAttribute(ServletContextRequest.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) { + throw new DBWebException("File ID not found"); + } + try { + // file id must be UUID + UUID.fromString(fileId); + } catch (IllegalArgumentException e) { + throw new DBWebException("File ID is invalid"); + } + 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..da50ce2c91 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -20,6 +20,7 @@ import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebSessionProvider; +import io.cloudbeaver.server.WebAppUtils; import io.cloudbeaver.server.jobs.SqlOutputLogReaderJob; import org.eclipse.jface.text.Document; import org.jkiss.code.NotNull; @@ -40,19 +41,23 @@ import org.jkiss.dbeaver.model.impl.DefaultServerOutputReader; import org.jkiss.dbeaver.model.navigator.DBNDatabaseItem; import org.jkiss.dbeaver.model.navigator.DBNNode; +import org.jkiss.dbeaver.model.qm.QMUtils; 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; import org.jkiss.dbeaver.model.struct.*; +import org.jkiss.dbeaver.model.websocket.event.WSTransactionalCountEvent; import org.jkiss.dbeaver.utils.GeneralUtils; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -66,6 +71,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; @@ -158,7 +166,8 @@ public WebSQLExecuteInfo processQuery( @Nullable WebSQLDataFilter filter, @Nullable WebDataFormat dataFormat, @NotNull WebSession webSession, - boolean readLogs) throws DBWebException { + boolean readLogs + ) throws DBWebException, DBCException { if (filter == null) { // Use default filter filter = new WebSQLDataFilter(); @@ -166,7 +175,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(), syntaxManager, sql); DBCExecutionContext context = getExecutionContext(dataContainer); @@ -191,71 +200,97 @@ public WebSQLExecuteInfo processQuery( ruleManager, document); - SQLQuery sqlQuery = (SQLQuery) SQLScriptParser.extractActiveQuery(parserContext, 0, sql.length()); + SQLScriptElement element = 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(); - } - 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()); + if (element instanceof SQLControlCommand command) { + SQLControlResult controlResult = dataContainer.getScriptContext().executeControlCommand(monitor, command); + if (controlResult.getTransformed() != null) { + element = controlResult.getTransformed(); + } else { + WebSQLQueryResults stats = new WebSQLQueryResults(webSession, dataFormat); + executeInfo.setResults(new WebSQLQueryResults[]{stats}); + } + } + if (element instanceof SQLQuery mainQuery) { + DBExecUtils.tryExecuteRecover(monitor, connection.getDataSource(), param -> { + try (DBCSession session = context.openSession(monitor, resolveQueryPurpose(dataFilter), "Execute SQL")) { + List sqlQueries = mainQuery.getScriptElements(); + for (SQLScriptElement sqlElement : sqlQueries) { + if (!(sqlElement instanceof SQLQuery sqlQuery)) { + log.error("Non-query script elements are not allowed: " + sqlElement); + continue; } - } - boolean hasResultSet = dbStat.executeStatement(); + AbstractExecutionSource source = new AbstractExecutionSource( + dataContainer, + session.getExecutionContext(), + WebSQLProcessor.this, + sqlQuery); - // Wait SqlLogStateJob, if its starts - if (sqlOutputLogReaderJob != null) { - sqlOutputLogReaderJob.join(); + 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(); + } + // Set query timeout + int queryTimeout = session.getDataSource().getContainer().getPreferenceStore() + .getInt(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(); + + // 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); } - } - }); + }); + } else { + executeInfo.setResults(new WebSQLQueryResults[0]); + } } catch (DBException e) { throw new DBWebException("Error executing query", e); } + DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); + if (txnManager != null && !txnManager.isAutoCommit()) { + sendTransactionalEvent(contextInfo); + } + executeInfo.setDuration(System.currentTimeMillis() - startTime); if (executeInfo.getResults().length == 0) { executeInfo.setStatusMessage("No Data"); } else { - executeInfo.setStatusMessage("Success"); + executeInfo.setStatusMessage("Executed"); } return executeInfo; @@ -268,11 +303,12 @@ public WebSQLExecuteInfo readDataFromContainer( @NotNull DBSDataContainer dataContainer, @Nullable String resultId, @NotNull WebSQLDataFilter filter, - @Nullable WebDataFormat dataFormat) throws DBException { + @Nullable WebDataFormat dataFormat + ) throws DBException { WebSQLExecuteInfo executeInfo = new WebSQLExecuteInfo(); - DBCExecutionContext executionContext = getExecutionContext(dataContainer); + DBCExecutionContext executionContext = DBUtils.getOrOpenDefaultContext(dataContainer, false); DBDDataFilter dataFilter = filter.makeDataFilter((resultId == null ? null : contextInfo.getResults(resultId))); DBExecUtils.tryExecuteRecover(monitor, connection.getDataSource(), param -> { try (DBCSession session = executionContext.openSession(monitor, resolveQueryPurpose(dataFilter), "Read data from container")) { @@ -295,9 +331,9 @@ public WebSQLExecuteInfo readDataFromContainer( executeInfo.setResults(new WebSQLQueryResults[]{results}); setResultFilterText(dataContainer, session.getDataSource(), executeInfo, dataFilter); executeInfo.setFullQuery(statistics.getQueryText()); - if (resultSet != null && resultSet.getRows() != null) { + if (resultSet != null && resultSet.getRowsWithMetaData() != null && resultSet.getResultsInfo() != null) { resultSet.getResultsInfo().setQueryText(statistics.getQueryText()); - executeInfo.setStatusMessage(resultSet.getRows().length + " row(s) fetched"); + executeInfo.setStatusMessage(resultSet.getRowsWithMetaData().size() + " row(s) fetched"); } } catch (DBException e) { throw new InvocationTargetException(e); @@ -316,7 +352,9 @@ public WebSQLExecuteInfo updateResultsDataBatch( @Nullable List addedRows, @Nullable WebDataFormat dataFormat) throws DBException { - List newResultSetRows = new ArrayList<>(); + // we don't need to add same row several times + // (it can be when we update the row from RS with several tables) + Set newResultSetRows = new LinkedHashSet<>(); KeyDataReceiver keyReceiver = new KeyDataReceiver(contextInfo.getResults(resultsId)); WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); @@ -333,19 +371,32 @@ public WebSQLExecuteInfo updateResultsDataBatch( WebSQLExecuteInfo result = new WebSQLExecuteInfo(); List queryResults = new ArrayList<>(); + boolean isAutoCommitEnabled = true; + for (var rowIdentifier : rowIdentifierList) { Map resultBatches = new LinkedHashMap<>(); DBSDataManipulator dataManipulator = generateUpdateResultsDataBatch( monitor, resultsInfo, rowIdentifier, updatedRows, deletedRows, addedRows, dataFormat, resultBatches, keyReceiver); - DBCExecutionContext executionContext = getExecutionContext(dataManipulator); try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { DBCTransactionManager txnManager = DBUtils.getTransactionManager(executionContext); boolean revertToAutoCommit = false; - if (txnManager != null && txnManager.isSupportsTransactions() && txnManager.isAutoCommit()) { - txnManager.setAutoCommit(monitor, false); - revertToAutoCommit = true; + DBCSavepoint savepoint = null; + if (txnManager != null) { + isAutoCommitEnabled = txnManager.isAutoCommit(); + if (txnManager.isSupportsTransactions() && isAutoCommitEnabled) { + txnManager.setAutoCommit(monitor, false); + revertToAutoCommit = true; + } + if (!txnManager.isAutoCommit() && txnManager.supportsSavepoints()) { + try { + savepoint = txnManager.setSavepoint(monitor, null); + } catch (Throwable e) { + // May be savepoints not supported + log.debug("Can't set savepoint", e); + } + } } try { Map options = Collections.emptyMap(); @@ -355,31 +406,41 @@ public WebSQLExecuteInfo updateResultsDataBatch( keyReceiver.setRow(rowValues); DBCStatistics statistics = batch.execute(session, options); - // Patch result rows (adapt to web format) - for (int i = 0; i < rowValues.length; i++) { - rowValues[i] = WebSQLUtils.makeWebCellValue(webSession, resultsInfo.getAttributeByPosition(i), rowValues[i], dataFormat); - } - totalUpdateCount += statistics.getRowsUpdated(); result.setDuration(result.getDuration() + statistics.getExecuteTime()); - newResultSetRows.add(rowValues); + newResultSetRows.add(new WebSQLQueryResultSetRow(rowValues, null)); } - if (txnManager != null && txnManager.isSupportsTransactions()) { + if (txnManager != null && txnManager.isSupportsTransactions() && isAutoCommitEnabled) { txnManager.commit(session); } } catch (Exception e) { if (txnManager != null && txnManager.isSupportsTransactions()) { - txnManager.rollback(session, null); + txnManager.rollback(session, savepoint); } throw new DBCException("Error persisting data changes", e); } finally { - if (revertToAutoCommit) { - txnManager.setAutoCommit(monitor, true); + if (txnManager != null) { + if (revertToAutoCommit) { + txnManager.setAutoCommit(monitor, true); + } + try { + if (savepoint != null) { + txnManager.releaseSavepoint(monitor, savepoint); + } + } catch (Throwable e) { + // Maybe savepoints not supported + log.debug("Can't release savepoint", e); + } } } } } + getUpdatedRowsInfo(resultsInfo, newResultSetRows, dataFormat, monitor); + + if (!isAutoCommitEnabled) { + sendTransactionalEvent(contextInfo); + } WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet(); updatedResultSet.setResultsInfo(resultsInfo); @@ -388,7 +449,7 @@ public WebSQLExecuteInfo updateResultsDataBatch( WebSQLQueryResults updateResults = new WebSQLQueryResults(webSession, dataFormat); updateResults.setUpdateRowCount(totalUpdateCount); updateResults.setResultSet(updatedResultSet); - updatedResultSet.setRows(newResultSetRows.toArray(new Object[0][])); + updatedResultSet.setRows(List.of(newResultSetRows.toArray(new WebSQLQueryResultSetRow[0]))); queryResults.add(updateResults); @@ -397,6 +458,95 @@ public WebSQLExecuteInfo updateResultsDataBatch( return result; } + private void sendTransactionalEvent(WebSQLContextInfo contextInfo) { + int count = QMUtils.getTransactionState(getExecutionContext()).getUpdateCount(); + webSession.addSessionEvent( + new WSTransactionalCountEvent( + contextInfo.getWebSession().getSessionId(), + contextInfo.getWebSession().getUserId(), + contextInfo.getProjectId(), + contextInfo.getId(), + contextInfo.getConnectionId(), + count + ) + ); + } + + private void getUpdatedRowsInfo( + @NotNull WebSQLResultsInfo resultsInfo, + @NotNull Set newResultSetRows, + @Nullable WebDataFormat dataFormat, + @NotNull DBRProgressMonitor monitor) + throws DBCException { + try (DBCSession session = getExecutionContext().openSession( + monitor, + DBCExecutionPurpose.UTIL, + "Refresh row(s) after insert/update") + ) { + boolean canRefreshResults = resultsInfo.canRefreshResults(); + for (WebSQLQueryResultSetRow row : newResultSetRows) { + if (row.getData().length == 0) { + continue; + } + if (!canRefreshResults) { + makeWebCellRow(resultsInfo, row, dataFormat); + continue; + } + List constraints = new ArrayList<>(); + boolean hasKey = true; + // get attributes only from row identifiers + Set idAttributes = resultsInfo.getRowIdentifiers().stream() + .flatMap(r -> r.getAttributes().stream()) + .collect(Collectors.toSet()); + for (DBDAttributeBinding attr : idAttributes) { + if (attr.getRowIdentifier() == null) { + continue; + } + final Object keyValue = row.getData()[attr.getOrdinalPosition()]; + if (DBUtils.isNullValue(keyValue)) { + hasKey = false; + break; + } + final DBDAttributeConstraint constraint = new DBDAttributeConstraint(attr); + constraint.setOperator(DBCLogicalOperator.EQUALS); + constraint.setValue(keyValue); + constraints.add(constraint); + } + if (!hasKey) { + // No key value for this row + makeWebCellRow(resultsInfo, row, dataFormat); + continue; + } + DBDDataFilter filter = new DBDDataFilter(constraints); + DBSDataContainer dataContainer = resultsInfo.getDataContainer(); + WebRowDataReceiver dataReceiver = new WebRowDataReceiver(resultsInfo.getAttributes(), row.getData(), dataFormat); + dataContainer.readData( + new AbstractExecutionSource(dataContainer, getExecutionContext(dataContainer), this), + session, + dataReceiver, + filter, + 0, + 0, + DBSDataContainer.FLAG_REFRESH, + 0); + } + } + } + + private void makeWebCellRow( + @NotNull WebSQLResultsInfo resultsInfo, + @NotNull WebSQLQueryResultSetRow row, + @Nullable WebDataFormat dataFormat + ) throws DBCException { + for (int i = 0; i < row.getData().length; i++) { + row.getData()[i] = WebSQLUtils.makeWebCellValue( + webSession, + resultsInfo.getAttributeByPosition(i), + row.getData()[i], + dataFormat); + } + } + public String generateResultsDataUpdateScript( @NotNull DBRProgressMonitor monitor, @NotNull WebSQLContextInfo contextInfo, @@ -473,15 +623,23 @@ private DBSDataManipulator generateUpdateResultsDataBatch( if (!CommonUtils.isEmpty(updatedRows)) { for (WebSQLResultsRow row : updatedRows) { + Object[] finalRow = row.getData(); 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)); - if (CommonUtils.isEmpty(row.getData()) || CommonUtils.isEmpty(updateValues)) { + .collect(HashMap::new, (m,v) -> m.put(v.getKey(), v.getValue()), HashMap::putAll); + + Map metaData; + if (row.getMetaData() != null) { + metaData = new HashMap<>(row.getMetaData()); + } else { + metaData = new HashMap<>(); + } + + if (finalRow.length == 0 || CommonUtils.isEmpty(updateValues)) { continue; } DBDAttributeBinding[] updateAttributes = new DBDAttributeBinding[updateValues.size()]; // Final row is what we return back - Object[] finalRow = row.getData().toArray(); int index = 0; for (String indexStr : updateValues.keySet()) { @@ -490,25 +648,26 @@ 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); - rowValues[i] = realCellValue; - finalRow[updateAttribute.getOrdinalPosition()] = realCellValue; - } + // put key values first in case of updating them + DBDDocument document = null; for (int i = 0; i < keyAttributes.length; i++) { DBDAttributeBinding keyAttribute = keyAttributes[i]; boolean isDocumentValue = keyAttributes.length == 1 && keyAttribute.getDataKind() == DBPDataKind.DOCUMENT && dataContainer instanceof DBSDocumentLocator; if (isDocumentValue) { - rowValues[updateAttributes.length + i] = - makeDocumentInputValue(session, (DBSDocumentLocator) dataContainer, resultsInfo, row); + document = makeDocumentInputValue( + session, + (DBSDocumentLocator) dataContainer, + resultsInfo, + row, + metaData + ); + rowValues[updateAttributes.length + i] = document; } else { rowValues[updateAttributes.length + i] = keyAttribute.getValueHandler().getValueFromObject( session, keyAttribute, convertInputCellValue(session, keyAttribute, - row.getData().get(keyAttribute.getOrdinalPosition()), withoutExecution), + row.getData()[(keyAttribute.getOrdinalPosition())], withoutExecution), false, true); } @@ -518,9 +677,19 @@ private DBSDataManipulator generateUpdateResultsDataBatch( finalRow[keyAttribute.getOrdinalPosition()] = rowValues[updateAttributes.length + i]; } } + for (int i = 0; i < updateAttributes.length; i++) { + DBDAttributeBinding updateAttribute = updateAttributes[i]; + Object value = updateValues.get(String.valueOf(updateAttribute.getOrdinalPosition())); + Object realCellValue = setCellRowValue(value, webSession, session, updateAttribute, withoutExecution); + if (document instanceof DBDComposite compositeDoc) { + compositeDoc.setAttributeValue(updateAttribute, realCellValue); + } + rowValues[i] = realCellValue; + finalRow[updateAttribute.getOrdinalPosition()] = realCellValue; + } DBSDataManipulator.ExecuteBatch updateBatch = dataManipulator.updateData( - session, updateAttributes, keyAttributes, keyReceiver, executionSource); + session, updateAttributes, keyAttributes, null, executionSource); updateBatch.add(rowValues); resultBatches.put(updateBatch, finalRow); } @@ -529,58 +698,80 @@ private DBSDataManipulator generateUpdateResultsDataBatch( // Add new rows if (!CommonUtils.isEmpty(addedRows)) { for (WebSQLResultsRow row : addedRows) { - List addedValues = row.getData(); - if (CommonUtils.isEmpty(row.getData())) { + Object[] addedValues = row.getData(); + if (addedValues.length == 0) { continue; } Map insertAttributes = new LinkedHashMap<>(); // Final row is what we return back - Object[] finalRow = row.getData().toArray(); for (int i = 0; i < allAttributes.length; i++) { - if (addedValues.get(i) != null) { - Object realCellValue = convertInputCellValue(session, allAttributes[i], - addedValues.get(i), withoutExecution); + if (addedValues[i] != null) { + Object realCellValue; + if (addedValues[i] instanceof Map variables) { + realCellValue = setCellRowValue(variables, webSession, session, allAttributes[i], withoutExecution); + } else { + realCellValue = convertInputCellValue(session, allAttributes[i], + addedValues[i], withoutExecution); + } insertAttributes.put(allAttributes[i], realCellValue); - finalRow[i] = realCellValue; + addedValues[i] = realCellValue; } } DBSDataManipulator.ExecuteBatch insertBatch = dataManipulator.insertData( session, insertAttributes.keySet().toArray(new DBDAttributeBinding[0]), - keyReceiver, + needKeys(keyAttributes, addedValues) ? keyReceiver : null, executionSource, new LinkedHashMap<>()); insertBatch.add(insertAttributes.values().toArray()); - resultBatches.put(insertBatch, finalRow); + resultBatches.put(insertBatch, addedValues); } } if (keyAttributes.length > 0 && !CommonUtils.isEmpty(deletedRows)) { for (WebSQLResultsRow row : deletedRows) { - List keyData = row.getData(); - if (CommonUtils.isEmpty(row.getData())) { + Object[] keyData = row.getData(); + Map keyMetaData = row.getMetaData(); + if (keyData.length == 0) { continue; } Map delKeyAttributes = new LinkedHashMap<>(); boolean isDocumentKey = keyAttributes.length == 1 && keyAttributes[0].getDataKind() == DBPDataKind.DOCUMENT; - for (int i = 0; i < allAttributes.length; i++) { - if (isDocumentKey || ArrayUtils.contains(keyAttributes, allAttributes[i])) { - Object realCellValue = convertInputCellValue(session, allAttributes[i], - keyData.get(i), withoutExecution); - delKeyAttributes.put(allAttributes[i], realCellValue); + if (dataContainer instanceof DBSDocumentLocator dataLocator) { + Map keyMap = new LinkedHashMap<>(); + DBDAttributeBinding[] attributes = resultsInfo.getAttributes(); + for (int j = 0; j < attributes.length; j++) { + DBDAttributeBinding attr = attributes[j]; + Object plainValue = WebSQLUtils.makePlainCellValue(session, attr, row.getData()[j]); + keyMap.put(attr.getName(), plainValue); } - } + DBDDocument document = dataLocator.findDocument(session, keyMap, keyMetaData); - DBSDataManipulator.ExecuteBatch deleteBatch = dataManipulator.deleteData( - session, - delKeyAttributes.keySet().toArray(new DBSAttributeBase[0]), - executionSource); - deleteBatch.add(delKeyAttributes.values().toArray()); - resultBatches.put(deleteBatch, new Object[0]); + DBSDataManipulator.ExecuteBatch deleteBatch = dataManipulator.deleteData( + session, + keyAttributes, + executionSource); + deleteBatch.add(new Object[] {document}); + resultBatches.put(deleteBatch, new Object[0]); + } else { + for (int i = 0; i < allAttributes.length; i++) { + if (isDocumentKey || ArrayUtils.contains(keyAttributes, allAttributes[i])) { + Object realCellValue = convertInputCellValue(session, allAttributes[i], + keyData[i], withoutExecution); + delKeyAttributes.put(allAttributes[i], realCellValue); + } + } + DBSDataManipulator.ExecuteBatch deleteBatch = dataManipulator.deleteData( + session, + delKeyAttributes.keySet().toArray(new DBSAttributeBase[0]), + executionSource); + deleteBatch.add(delKeyAttributes.values().toArray()); + resultBatches.put(deleteBatch, new Object[0]); + } } } } @@ -588,12 +779,22 @@ private DBSDataManipulator generateUpdateResultsDataBatch( return dataManipulator; } + private boolean needKeys(DBDAttributeBinding[] keyAttributes, Object[] finalRow) { + for (var col : keyAttributes) { + if (col.getAttribute().isAutoGenerated() && DBUtils.isNullValue(finalRow[col.getOrdinalPosition()])) { + return true; + } + } + return false; + } + @NotNull public DBDDocument makeDocumentInputValue( DBCSession session, DBSDocumentLocator dataContainer, WebSQLResultsInfo resultsInfo, - WebSQLResultsRow row) throws DBException + WebSQLResultsRow row, + Map metaData) throws DBException { // Document reference DBDDocument document = null; @@ -601,17 +802,19 @@ public DBDDocument makeDocumentInputValue( DBDAttributeBinding[] attributes = resultsInfo.getAttributes(); for (int j = 0; j < attributes.length; j++) { DBDAttributeBinding attr = attributes[j]; - Object plainValue = WebSQLUtils.makePlainCellValue(session, attr, row.getData().get(j)); - if (plainValue instanceof DBDDocument) { + Object plainValue = WebSQLUtils.makePlainCellValue(session, attr, row.getData()[j]); + if (plainValue instanceof DBDDocument dbdDocument) { // FIXME: Hack for DynamoDB. We pass entire document as a key // FIXME: Let's just return it back for now - document = (DBDDocument) plainValue; - break; + if (dataContainer.isDocumentValid(dbdDocument)) { + document = (DBDDocument) plainValue; + break; + } } keyMap.put(attr.getName(), plainValue); } if (document == null) { - document = dataContainer.findDocument(session.getProgressMonitor(), keyMap); + document = dataContainer.findDocument(session, keyMap, metaData); if (document == null) { throw new DBCException("Error finding document by key " + keyMap); } @@ -632,6 +835,10 @@ public Object convertInputCellValue(DBCSession session, DBDAttributeBinding upda cellRawValue, false, true); + //FIXME: fix array editing for nosql databases + if (realCellValue == null && cellRawValue != null && updateAttribute.getDataKind() == DBPDataKind.ARRAY) { + throw new DBCException("Array update is not supported"); + } } catch (DBCException e) { //checks if this function is used only for script generation if (justGenerateScript) { @@ -692,54 +899,103 @@ public String readLobValue( @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, @NotNull Integer lobColumnIndex, - @Nullable WebSQLResultsRow row + @NotNull WebSQLResultsRow row ) throws DBException { WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); DBDRowIdentifier rowIdentifier = resultsInfo.getDefaultRowIdentifier(); - checkRowIdentifier(resultsInfo, rowIdentifier); + String tableName; + if (rowIdentifier == null && resultsInfo.isSingleRow()) { + tableName = resultsInfo.getDataContainer().getName(); + } else { + checkRowIdentifier(resultsInfo, rowIdentifier); + tableName = rowIdentifier.getEntity().getName(); + } + WebSQLDataLOBReceiver dataReceiver = new WebSQLDataLOBReceiver(tableName, resultsInfo.getDataContainer(), lobColumnIndex); + readCellDataValue(monitor, resultsInfo, row, dataReceiver); + try { + return dataReceiver.createLobFile(monitor); + } catch (Exception e) { + throw new DBWebException("Error creating temporary lob file ", e); + } + } + + private void readCellDataValue( + @NotNull DBRProgressMonitor monitor, + @NotNull WebSQLResultsInfo resultsInfo, + @Nullable WebSQLResultsRow row, + @NotNull WebSQLCellValueReceiver dataReceiver + ) throws DBException { DBSDataContainer dataContainer = resultsInfo.getDataContainer(); DBCExecutionContext executionContext = getExecutionContext(dataContainer); - String tableName = rowIdentifier.getEntity().getName(); - WebSQLDataLOBReceiver dataReceiver = new WebSQLDataLOBReceiver(tableName, dataContainer, lobColumnIndex); try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Generate data update batches")) { - WebExecutionSource executionSource = new WebExecutionSource(dataContainer, executionContext, this); DBDDataFilter dataFilter = new DBDDataFilter(); - DBDAttributeBinding[] keyAttributes = rowIdentifier.getAttributes().toArray(new DBDAttributeBinding[0]); - Object[] rowValues = new Object[keyAttributes.length]; - List constraints = new ArrayList<>(); - for (int i = 0; i < keyAttributes.length; i++) { - DBDAttributeBinding keyAttribute = keyAttributes[i]; - boolean isDocumentValue = keyAttributes.length == 1 && keyAttribute.getDataKind() == DBPDataKind.DOCUMENT && dataContainer instanceof DBSDocumentLocator; - if (isDocumentValue) { - rowValues[i] = - makeDocumentInputValue(session, (DBSDocumentLocator) dataContainer, resultsInfo, row); - } else { - Object inputCellValue = row.getData().get(keyAttribute.getOrdinalPosition()); - - rowValues[i] = keyAttribute.getValueHandler().getValueFromObject( - session, - keyAttribute, - convertInputCellValue(session, keyAttribute, - inputCellValue, false), - false, - true); - } - final DBDAttributeConstraint constraint = new DBDAttributeConstraint(keyAttribute); - constraint.setOperator(DBCLogicalOperator.EQUALS); - constraint.setValue(rowValues[i]); - constraints.add(constraint); + if (!resultsInfo.isSingleRow()) { + addKeyAttributes(resultsInfo, row, dataContainer, session, dataFilter); } - dataFilter.addConstraints(constraints); - DBCStatistics statistics = dataContainer.readData( + WebExecutionSource executionSource = new WebExecutionSource(dataContainer, executionContext, this); + dataContainer.readData( executionSource, session, dataReceiver, dataFilter, 0, 1, DBSDataContainer.FLAG_NONE, 1); - try { - return dataReceiver.createLobFile(session); - } catch (Exception e) { - throw new DBWebException("Error creating temporary lob file ", e); + } + } + + private void addKeyAttributes( + @NotNull WebSQLResultsInfo resultsInfo, + @Nullable WebSQLResultsRow row, + @NotNull DBSDataContainer dataContainer, + @NotNull DBCSession session, + @NotNull DBDDataFilter dataFilter + ) throws DBException { + DBDAttributeBinding[] keyAttributes = resultsInfo.getDefaultRowIdentifier().getAttributes().toArray(new DBDAttributeBinding[0]); + Object[] rowValues = new Object[keyAttributes.length]; + List constraints = new ArrayList<>(); + for (int i = 0; i < keyAttributes.length; i++) { + DBDAttributeBinding keyAttribute = keyAttributes[i]; + boolean isDocumentValue = keyAttributes.length == 1 + && keyAttribute.getDataKind() == DBPDataKind.DOCUMENT + && dataContainer instanceof DBSDocumentLocator; + if (isDocumentValue) { + rowValues[i] = + makeDocumentInputValue(session, (DBSDocumentLocator) dataContainer, resultsInfo, row, null); + } else { + Object inputCellValue = row.getData()[keyAttribute.getOrdinalPosition()]; + + rowValues[i] = keyAttribute.getValueHandler().getValueFromObject( + session, + keyAttribute, + convertInputCellValue(session, keyAttribute, + inputCellValue, false), + false, + true); } + final DBDAttributeConstraint constraint = new DBDAttributeConstraint(keyAttribute); + constraint.setOperator(DBCLogicalOperator.EQUALS); + constraint.setValue(rowValues[i]); + constraints.add(constraint); + } + dataFilter.addConstraints(constraints); + } + + /** + * Reads cell value as string from provided row and column index. + */ + @NotNull + public String readStringValue( + @NotNull DBRProgressMonitor monitor, + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @NotNull Integer columnIndex, + @NotNull WebSQLResultsRow row + ) throws DBException { + WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); + if (!resultsInfo.isSingleRow()) { + DBDRowIdentifier rowIdentifier = resultsInfo.getDefaultRowIdentifier(); + checkRowIdentifier(resultsInfo, rowIdentifier); } + WebSQLCellValueReceiver dataReceiver = new WebSQLCellValueReceiver(resultsInfo.getDataContainer(), columnIndex); + readCellDataValue(monitor, resultsInfo, row, dataReceiver); + return new String(dataReceiver.getBinaryValue(monitor), StandardCharsets.UTF_8); } //////////////////////////////////////////////// @@ -759,7 +1015,7 @@ private void checkDataEditAllowed(DBSEntity dataContainer) throws DBWebException @NotNull public T getDataContainerByNodePath(DBRProgressMonitor monitor, @NotNull String containerPath, Class type) throws DBException { - DBNNode node = webSession.getNavigatorModel().getNodeByPath(monitor, containerPath); + DBNNode node = webSession.getNavigatorModelOrThrow().getNodeByPath(monitor, containerPath); if (node == null) { throw new DBWebException("Container node '" + containerPath + "' not found"); } @@ -787,7 +1043,7 @@ private void fillQueryResults( List resultList = new ArrayList<>(); int maxResultsCount = resolveMaxResultsCount(dataContainer.getDataSource()); WebSQLQueryResults stats = new WebSQLQueryResults(webSession, dataFormat); - var rowsUpdated = 0; + long rowsUpdated = 0; for (int i = 0; i < maxResultsCount; i++) { if (hasResultSet) { WebSQLQueryResults results = new WebSQLQueryResults(webSession, dataFormat); @@ -866,17 +1122,17 @@ void setRow(Object[] row) { } @Override - public void fetchStart(DBCSession session, DBCResultSet resultSet, long offset, long maxRows) { + public void fetchStart(@NotNull DBCSession session, @NotNull DBCResultSet resultSet, long offset, long maxRows) { } @Override - public void fetchRow(DBCSession session, DBCResultSet resultSet) + public void fetchRow(@NotNull DBCSession session, @NotNull DBCResultSet resultSet) throws DBCException { DBDAttributeBinding[] resultsAttributes = results.getAttributes(); DBCResultSetMetaData rsMeta = resultSet.getMeta(); - List keyAttributes = rsMeta.getAttributes(); + List keyAttributes = rsMeta.getAttributes(); for (int i = 0; i < keyAttributes.size(); i++) { DBCAttributeMetaData keyAttribute = keyAttributes.get(i); DBDValueHandler valueHandler = DBUtils.findValueHandler(session, keyAttribute); @@ -907,7 +1163,7 @@ public void fetchRow(DBCSession session, DBCResultSet resultSet) } @Override - public void fetchEnd(DBCSession session, DBCResultSet resultSet) { + public void fetchEnd(@NotNull DBCSession session, @NotNull DBCResultSet resultSet) { } @@ -916,6 +1172,30 @@ public void close() { } } + public class WebRowDataReceiver extends RowDataReceiver { + private final WebDataFormat dataFormat; + + public WebRowDataReceiver(DBDAttributeBinding[] curAttributes, Object[] rowValues, WebDataFormat dataFormat) { + super(curAttributes); + this.rowValues = rowValues; + this.dataFormat = dataFormat; + } + + @Override + protected void fetchRowValues(DBCSession session, DBCResultSet resultSet) throws DBCException { + for (int i = 0; i < curAttributes.length; i++) { + final DBDAttributeBinding attr = curAttributes[i]; + DBDValueHandler valueHandler = attr.getValueHandler(); + Object attrValue = valueHandler.fetchValueObject(session, resultSet, attr, i); + + // Patch result rows (adapt to web format) + rowValues[i] = WebSQLUtils.makeWebCellValue(webSession, attr, attrValue, dataFormat); + } + } + + } + + /////////////////////////////////////////////////////// // Utils private static int resolveMaxResultsCount(@Nullable DBPDataSource dataSource) { @@ -928,4 +1208,25 @@ 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) + throws DBException { + if (cellRow instanceof Map) { + Map variables = (Map) cellRow; + if (variables.get(FILE_ID) != null) { + Path path = WebAppUtils.getWebPlatform() + .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) { + throw new DBException(e.getMessage()); + } + } + } + return convertInputCellValue(dbcSession, allAttributes, cellRow, withoutExecution); + } } 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..1529bbde64 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -18,6 +18,7 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPContextProvider; import org.jkiss.dbeaver.model.DBPDataSource; @@ -27,7 +28,9 @@ import org.jkiss.dbeaver.model.exec.*; import org.jkiss.dbeaver.model.sql.SQLQuery; import org.jkiss.dbeaver.model.sql.SQLScriptContext; +import org.jkiss.dbeaver.model.sql.SQLSyntaxManager; import org.jkiss.dbeaver.model.sql.data.SQLQueryDataContainer; +import org.jkiss.dbeaver.model.sql.transformers.SQLQueryTransformerCount; import org.jkiss.dbeaver.model.struct.DBSDataContainer; import org.jkiss.dbeaver.model.struct.DBSObject; @@ -41,11 +44,13 @@ public class WebSQLQueryDataContainer implements DBSDataContainer, DBPContextPro private static final Log log = Log.getLog(WebSQLQueryDataContainer.class); private final DBPDataSource dataSource; + private final SQLSyntaxManager syntaxManager; private final String query; private final SQLQueryDataContainer queryDataContainer; - public WebSQLQueryDataContainer(DBPDataSource dataSource, String query) { + public WebSQLQueryDataContainer(DBPDataSource dataSource, SQLSyntaxManager syntaxManager, String query) { this.dataSource = dataSource; + this.syntaxManager = syntaxManager; this.query = query; SQLScriptContext scriptContext = new SQLScriptContext(null, @@ -89,13 +94,27 @@ public String[] getSupportedFeatures() { @NotNull @Override - public DBCStatistics readData(@NotNull DBCExecutionSource source, @NotNull DBCSession session, @NotNull DBDDataReceiver dataReceiver, @Nullable DBDDataFilter dataFilter, long firstRow, long maxRows, long flags, int fetchSize) throws DBCException { + public DBCStatistics readData( + @Nullable DBCExecutionSource source, + @NotNull DBCSession session, + @NotNull DBDDataReceiver dataReceiver, + @Nullable DBDDataFilter dataFilter, + long firstRow, + long maxRows, + long flags, + int fetchSize + ) throws DBCException { return queryDataContainer.readData(source, session, dataReceiver, dataFilter, firstRow, maxRows, flags, fetchSize); } @Override public long countData(@NotNull DBCExecutionSource source, @NotNull DBCSession session, @Nullable DBDDataFilter dataFilter, long flags) throws DBCException { - return queryDataContainer.countData(source, session, dataFilter, flags); + try { + SQLQuery countQuery = new SQLQueryTransformerCount().transformQuery(dataSource, syntaxManager, new SQLQuery(dataSource, query)); + return DBUtils.countDataFromQuery(source, session, countQuery); + } catch (DBException e) { + throw new DBCException("Error executing row count", e); + } } @Nullable @@ -104,4 +123,9 @@ public DBCExecutionContext getExecutionContext() { return DBUtils.getDefaultContext(dataSource, false); } + @NotNull + public SQLScriptContext getScriptContext() { + return queryDataContainer.getScriptContext(); + } + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java index 4a195bc00a..5a6a38041a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,21 +17,26 @@ package io.cloudbeaver.service.sql; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.utils.ServletAppUtils; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPDataKind; import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.data.*; import org.jkiss.dbeaver.model.exec.*; +import org.jkiss.dbeaver.model.exec.trace.DBCTrace; +import org.jkiss.dbeaver.model.exec.trace.DBCTraceDynamic; import org.jkiss.dbeaver.model.impl.data.DBDValueError; +import org.jkiss.dbeaver.model.meta.MetaData; import org.jkiss.dbeaver.model.sql.DBQuotaException; import org.jkiss.dbeaver.model.struct.DBSDataContainer; import org.jkiss.dbeaver.model.struct.DBSEntity; import org.jkiss.utils.CommonUtils; -import java.util.ArrayList; -import java.util.List; +import java.lang.reflect.Method; +import java.util.*; +import java.util.stream.Collectors; class WebSQLQueryDataReceiver implements DBDDataReceiver { private static final Log log = Log.getLog(WebSQLQueryDataReceiver.class); @@ -42,14 +47,17 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver { private final WebSQLQueryResultSet webResultSet = new WebSQLQueryResultSet(); private DBDAttributeBinding[] bindings; - private List rows = new ArrayList<>(); + private DBCTrace trace; + private List rows = new ArrayList<>(); private final Number rowLimit; WebSQLQueryDataReceiver(WebSQLContextInfo contextInfo, DBSDataContainer dataContainer, WebDataFormat dataFormat) { this.contextInfo = contextInfo; this.dataContainer = dataContainer; this.dataFormat = dataFormat; - rowLimit = CBApplication.getInstance().getAppConfiguration().getResourceQuota(WebSQLConstants.QUOTA_PROP_ROW_LIMIT); + rowLimit = ServletAppUtils.getServletApplication() + .getAppConfiguration() + .getResourceQuota(WebSQLConstants.QUOTA_PROP_ROW_LIMIT); } public WebSQLQueryResultSet getResultSet() { @@ -57,19 +65,23 @@ public WebSQLQueryResultSet getResultSet() { } @Override - public void fetchStart(DBCSession session, DBCResultSet dbResult, long offset, long maxRows) throws DBCException { + public void fetchStart(@NotNull DBCSession session, @NotNull DBCResultSet dbResult, long offset, long maxRows) throws DBCException { DBCResultSetMetaData meta = dbResult.getMeta(); - List attributes = meta.getAttributes(); + List attributes = meta.getAttributes(); bindings = new DBDAttributeBindingMeta[attributes.size()]; for (int i = 0; i < attributes.size(); i++) { DBCAttributeMetaData attrMeta = attributes.get(i); bindings[i] = new DBDAttributeBindingMeta(dataContainer, dbResult.getSession(), attrMeta); } + if (dbResult instanceof DBCResultSetTrace resultSetTrace) { + this.trace = resultSetTrace.getExecutionTrace(); + } } @Override - public void fetchRow(DBCSession session, DBCResultSet resultSet) throws DBCException { + public void fetchRow(@NotNull DBCSession session, @NotNull DBCResultSet resultSet) throws DBCException { + Map metaDataMap = null; Object[] row = new Object[bindings.length]; for (int i = 0; i < bindings.length; i++) { @@ -81,12 +93,25 @@ public void fetchRow(DBCSession session, DBCResultSet resultSet) throws DBCExcep binding.getMetaAttribute(), i); row[i] = cellValue; + if (cellValue != null) { + Method[] methods = cellValue.getClass().getMethods(); + for (Method method : methods) { + if (method.isAnnotationPresent(MetaData.class)) { + if (metaDataMap == null) { + metaDataMap = new HashMap<>(); + } + Object value = method.invoke(cellValue); + metaDataMap.put(method.getAnnotation(MetaData.class).name(), value); + } + } + } + } catch (Throwable e) { row[i] = new DBDValueError(e); } } - rows.add(row); + rows.add(new WebSQLQueryResultSetRow(row, metaDataMap)); if (rowLimit != null && rows.size() > rowLimit.longValue()) { throw new DBQuotaException( @@ -95,13 +120,13 @@ public void fetchRow(DBCSession session, DBCResultSet resultSet) throws DBCExcep } @Override - public void fetchEnd(DBCSession session, DBCResultSet resultSet) throws DBCException { + public void fetchEnd(@NotNull DBCSession session, @NotNull DBCResultSet resultSet) throws DBCException { WebSession webSession = contextInfo.getProcessor().getWebSession(); DBSEntity entity = dataContainer instanceof DBSEntity ? (DBSEntity) dataContainer : null; try { - DBExecUtils.bindAttributes(session, entity, resultSet, bindings, rows); + DBExecUtils.bindAttributes(session, entity, resultSet, bindings, rows.stream().map(WebSQLQueryResultSetRow::getData).collect(Collectors.toList())); } catch (DBException e) { log.error("Error binding attributes", e); } @@ -122,25 +147,30 @@ public void fetchEnd(DBCSession session, DBCResultSet resultSet) throws DBCExcep } // Convert row values - for (Object[] row : rows) { + for (WebSQLQueryResultSetRow row : rows) { for (int i = 0; i < bindings.length; i++) { DBDAttributeBinding binding = bindings[i]; - row[i] = WebSQLUtils.makeWebCellValue(webSession, binding, row[i], dataFormat); + row.getData()[i] = WebSQLUtils.makeWebCellValue(webSession, binding, row.getData()[i], dataFormat); } } webResultSet.setColumns(bindings); - webResultSet.setRows(rows.toArray(new Object[0][])); + webResultSet.setRows(List.of(rows.toArray(new WebSQLQueryResultSetRow[0]))); + webResultSet.setHasChildrenCollection(resultSet instanceof DBDSubCollectionResultSet); + webResultSet.setSupportsDataFilter(dataContainer.isFeatureSupported(DBSDataContainer.FEATURE_DATA_FILTER)); + webResultSet.setHasDynamicTrace(trace instanceof DBCTraceDynamic); + webResultSet.setReadOnlyInfo(contextInfo.getProcessor().getExecutionContext()); - WebSQLResultsInfo resultsInfo = contextInfo.saveResult(dataContainer, bindings); + WebSQLResultsInfo resultsInfo = contextInfo.saveResult(dataContainer, trace, bindings, rows.size() == 1); webResultSet.setResultsInfo(resultsInfo); boolean isSingleEntity = DBExecUtils.detectSingleSourceTable(bindings) != null; webResultSet.setSingleEntity(isSingleEntity); - DBDRowIdentifier rowIdentifier = resultsInfo.getDefaultRowIdentifier(); - webResultSet.setHasRowIdentifier(rowIdentifier != null && rowIdentifier.isValidIdentifier()); + Set rowIdentifiers = resultsInfo.getRowIdentifiers(); + boolean hasRowIdentifier = rowIdentifiers.stream().allMatch(DBDRowIdentifier::isValidIdentifier); + webResultSet.setHasRowIdentifier(!rowIdentifiers.isEmpty() && hasRowIdentifier); } private void convertComplexValuesToRelationalView(DBCSession session) { @@ -157,14 +187,14 @@ private void convertComplexValuesToRelationalView(DBCSession session) { // Convert original rows into new rows with leaf attributes // Extract values for leaf attributes from original row DBDAttributeBinding[] leafAttributes = leafBindings.toArray(new DBDAttributeBinding[0]); - List newRows = new ArrayList<>(); - for (Object[] row : rows) { + List newRows = new ArrayList<>(); + for (WebSQLQueryResultSetRow row : rows) { Object[] newRow = new Object[leafBindings.size()]; for (int i = 0; i < leafBindings.size(); i++) { DBDAttributeBinding leafAttr = leafBindings.get(i); try { //Object topValue = row[leafAttr.getTopParent().getOrdinalPosition()]; - Object cellValue = DBUtils.getAttributeValue(leafAttr, leafAttributes, row); + Object cellValue = DBUtils.getAttributeValue(leafAttr, leafAttributes, row.getData()); /* Object cellValue = leafAttr.getValueHandler().getValueFromObject( session, @@ -178,7 +208,7 @@ private void convertComplexValuesToRelationalView(DBCSession session) { newRow[i] = new DBDValueError(e); } } - newRows.add(newRow); + newRows.add(new WebSQLQueryResultSetRow(newRow, row.getMetaData())); } this.bindings = leafAttributes; this.rows = newRows; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java index 9e599b0f21..a3a5778b3c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -49,7 +49,7 @@ public Integer getPosition() { @Property public String getName() { - return attrMeta.getFullyQualifiedName(DBPEvaluationContext.UI); + return WebSQLUtils.getColumnName(attrMeta); } @Property @@ -105,9 +105,19 @@ public boolean isRequired() { return attrMeta.isRequired(); } + @Property + public boolean isAutoGenerated() { + return attrMeta.isAutoGenerated(); + } + + @Property + public String getDescription() { + return attrMeta.getDescription(); + } + @Property public boolean isReadOnly() { - return DBExecUtils.isAttributeReadOnly(attrMeta); + return DBExecUtils.isAttributeReadOnly(attrMeta, true); } @Property diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java index 58ffad7a07..d7c48dd697 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,10 +16,16 @@ */ package io.cloudbeaver.service.sql; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.data.DBDAttributeBinding; +import org.jkiss.dbeaver.model.exec.DBCExecutionContext; +import org.jkiss.dbeaver.model.exec.DBExecUtils; import org.jkiss.dbeaver.model.meta.Property; +import java.util.Collections; +import java.util.List; + /** * Web SQL query resultset. */ @@ -28,12 +34,18 @@ public class WebSQLQueryResultSet { private static final Log log = Log.getLog(WebSQLQueryResultSet.class); private WebSQLQueryResultColumn[] columns; - private Object[][] rows; + private List rows = Collections.emptyList(); private boolean hasMoreData; private WebSQLResultsInfo resultsInfo; private boolean singleEntity = true; private boolean hasRowIdentifier; + private boolean hasChildrenCollection; + private boolean isSupportsDataFilter; + private boolean hasDynamicTrace; + private boolean readOnly; + private String readOnlyStatus; + public WebSQLQueryResultSet() { } @@ -60,11 +72,17 @@ public void setColumns(DBDAttributeBinding[] bindings) { } @Property + @Deprecated public Object[][] getRows() { + return rows.stream().map(WebSQLQueryResultSetRow::getData).toArray(x -> new Object[x][1]); + } + + @Property + public List getRowsWithMetaData() { return rows; } - public void setRows(Object[][] rows) { + public void setRows(List rows) { this.rows = rows; } @@ -102,4 +120,49 @@ public boolean isHasRowIdentifier() { public void setHasRowIdentifier(boolean hasRowIdentifier) { this.hasRowIdentifier = hasRowIdentifier; } + + @Property + public boolean isHasChildrenCollection() { + return hasChildrenCollection; + } + + public void setHasChildrenCollection(boolean hasSuCollection) { + this.hasChildrenCollection = hasSuCollection; + } + + @Property + public boolean isSupportsDataFilter() { + return isSupportsDataFilter; + } + + public void setSupportsDataFilter(boolean supportsDataFilter) { + isSupportsDataFilter = supportsDataFilter; + } + + @Property + public boolean isHasDynamicTrace() { + return hasDynamicTrace; + } + + public void setHasDynamicTrace(boolean hasDynamicTrace) { + this.hasDynamicTrace = hasDynamicTrace; + } + + @Property + public boolean isReadOnly() { + return readOnly; + } + + @Property + public String getReadOnlyStatus() { + return readOnlyStatus; + } + + /** + * Sets info about read-only status of a result set. + */ + public void setReadOnlyInfo(@NotNull DBCExecutionContext executionContext) { + this.readOnly = DBExecUtils.isResultSetReadOnly(executionContext); + this.readOnlyStatus = DBExecUtils.getResultSetReadOnlyStatus(executionContext.getDataSource().getContainer()); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSetRow.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSetRow.java new file mode 100644 index 0000000000..30b4b15966 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSetRow.java @@ -0,0 +1,31 @@ +package io.cloudbeaver.service.sql; + +import java.util.Map; + +public class WebSQLQueryResultSetRow { + + private Object[] data; + + private Map metaData; + + public WebSQLQueryResultSetRow(Object[] data, Map metaData) { + this.data = data; + this.metaData = metaData; + } + + public Object[] getData() { + return data; + } + + public Map getMetaData() { + return metaData; + } + + public void setData(Object[] data) { + this.data = data; + } + + public void setMetaData(Map metaData) { + this.metaData = metaData; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResults.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResults.java index 6792750f0f..1140ed2304 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResults.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResults.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -86,16 +86,16 @@ public List getDocuments() throws DBCException { } List documents = new ArrayList<>(); - for (Object[] row : resultSet.getRows()) { - if (row.length != 1) { + for (WebSQLQueryResultSetRow row : resultSet.getRowsWithMetaData()) { + if (row.getData().length != 1) { log.debug("Non-document row content"); } - if (row[0] == null) { + if (row.getData()[0] == null) { documents.add(null); - } else if (row[0] instanceof DBDDocument) { - documents.add(new WebSQLDatabaseDocument(webSession, (DBDDocument) row[0])); + } else if (row.getData()[0] instanceof DBDDocument) { + documents.add(new WebSQLDatabaseDocument(webSession, (DBDDocument) row.getData()[0])); } else { - log.debug("Non-document row value: " + row[0].getClass().getName()); + log.debug("Non-document row value: " + row.getData()[0].getClass().getName()); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java index bd55958c2e..196dbc3cae 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java @@ -1,22 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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 io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.app.ServletApplication; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.servlets.CBStaticServlet; +import io.cloudbeaver.server.CBConstants; import io.cloudbeaver.service.WebServiceServletBase; -import org.eclipse.jetty.server.Request; +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.MultipartConfig; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Part; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; -import javax.servlet.MultipartConfigElement; -import javax.servlet.ServletException; -import javax.servlet.annotation.MultipartConfig; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.Part; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -35,14 +51,14 @@ public class WebSQLResultServlet extends WebServiceServletBase { private final DBWServiceSQL sqlService; - public WebSQLResultServlet(CBApplication application, DBWServiceSQL sqlService) { + public WebSQLResultServlet(ServletApplication application, DBWServiceSQL sqlService) { super(application); this.sqlService = sqlService; } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG); + request.setAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG); String fileName = UUID.randomUUID().toString(); for (Part part : request.getParts()) { part.write(WebSQLDataLOBReceiver.DATA_EXPORT_FOLDER + "/" + fileName); @@ -72,8 +88,8 @@ protected void processServiceRequest(WebSession session, HttpServletRequest requ response.setHeader("Content-Type", "application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=\"" + dataFile.getFileName().toString() + "\""); response.setHeader("Content-Length", String.valueOf(Files.size(dataFile))); - response.setDateHeader("Expires", System.currentTimeMillis() + CBStaticServlet.STATIC_CACHE_SECONDS * 1000); - response.setHeader("Cache-Control", "public, max-age=" + CBStaticServlet.STATIC_CACHE_SECONDS); + response.setDateHeader("Expires", System.currentTimeMillis() + CBConstants.STATIC_CACHE_SECONDS * 1000); + response.setHeader("Cache-Control", "public, max-age=" + CBConstants.STATIC_CACHE_SECONDS); try (InputStream is = Files.newInputStream(dataFile)) { IOUtils.copyStream(is, response.getOutputStream()); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultsRow.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultsRow.java index 51fb364de2..d651f44179 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultsRow.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultsRow.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,9 +16,9 @@ */ package io.cloudbeaver.service.sql; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.data.json.JSONUtils; -import java.util.List; import java.util.Map; /** @@ -26,22 +26,31 @@ */ public class WebSQLResultsRow { - private List data; + private Object[] data; private Map updateValues; + @Nullable + private Map metaData; + public WebSQLResultsRow() { } public WebSQLResultsRow(Map map) { - data = JSONUtils.getObjectList(map, "data"); + data = JSONUtils.getObjectList(map, "data").toArray(); updateValues = JSONUtils.getObject(map, "updateValues"); + metaData = JSONUtils.getObject(map, "metaData"); } - public List getData() { + public Object[] getData() { return data; } public Map getUpdateValues() { return updateValues; } + + @Nullable + public Map getMetaData() { + return metaData; + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java index 1e46ba9b8b..c53143bf25 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,19 +16,24 @@ */ package io.cloudbeaver.service.sql; +import io.cloudbeaver.model.WebAsyncTaskInfo; +import io.cloudbeaver.model.app.ServletAppConfiguration; +import io.cloudbeaver.model.session.WebAsyncTaskProcessor; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebServiceRegistry; -import io.cloudbeaver.server.CBAppConfig; -import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.utils.CBModelConstants; +import io.cloudbeaver.utils.ServletAppUtils; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPEvaluationContext; import org.jkiss.dbeaver.model.data.*; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.exec.DBCSession; import org.jkiss.dbeaver.model.gis.DBGeometry; import org.jkiss.dbeaver.model.gis.GisConstants; import org.jkiss.dbeaver.model.gis.GisTransformUtils; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.struct.DBSAttributeBase; import org.jkiss.dbeaver.model.struct.DBSTypedObject; import org.jkiss.dbeaver.utils.ContentUtils; @@ -36,9 +41,9 @@ import org.jkiss.utils.Base64; import org.jkiss.utils.CommonUtils; -import java.io.ByteArrayOutputStream; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; import java.util.*; /** @@ -130,9 +135,9 @@ private static Map createMapOfType(String type) { private static Map serializeDocumentValue(WebSession session, DBDDocument document) throws DBCException { String documentData; try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.serializeDocument(session.getProgressMonitor(), baos, StandardCharsets.UTF_8); - documentData = new String(baos.toByteArray(), StandardCharsets.UTF_8); + StringWriter writer = new StringWriter(); + document.serializeDocument(session.getProgressMonitor(), writer); + documentData = writer.toString(); } catch (Exception e) { throw new DBCException("Error serializing document", e); } @@ -150,6 +155,15 @@ private static Object serializeContentValue(WebSession session, DBDContent value Map map = createMapOfType(WebSQLConstants.VALUE_TYPE_CONTENT); if (ContentUtils.isTextContent(value)) { String stringValue = ContentUtils.getContentStringValue(session.getProgressMonitor(), value); + int textPreviewMaxLength = CommonUtils.toInt( + ServletAppUtils.getServletApplication() + .getAppConfiguration() + .getResourceQuota(WebSQLConstants.QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH), + WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH + ); + if (stringValue != null && stringValue.length() > textPreviewMaxLength) { + stringValue = stringValue.substring(0, textPreviewMaxLength); + } map.put(WebSQLConstants.ATTR_TEXT, stringValue); } else { map.put(WebSQLConstants.ATTR_BINARY, true); @@ -157,12 +171,11 @@ private static Object serializeContentValue(WebSession session, DBDContent value if (binaryValue != null) { byte[] previewValue = binaryValue; // gets parameters from the configuration file - CBAppConfig config = CBApplication.getInstance().getAppConfiguration(); + ServletAppConfiguration config = ServletAppUtils.getServletApplication().getAppConfiguration(); // the max length of the text preview int textPreviewMaxLength = CommonUtils.toInt( config.getResourceQuota( - WebSQLConstants.QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH, - WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH)); + WebSQLConstants.QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH), WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH); if (previewValue.length > textPreviewMaxLength) { previewValue = Arrays.copyOf(previewValue, textPreviewMaxLength); } @@ -170,8 +183,8 @@ private static Object serializeContentValue(WebSession session, DBDContent value // the max length of the binary preview int binaryPreviewMaxLength = CommonUtils.toInt( config.getResourceQuota( - WebSQLConstants.QUOTA_PROP_BINARY_PREVIEW_MAX_LENGTH, - WebSQLConstants.BINARY_PREVIEW_MAX_LENGTH)); + WebSQLConstants.QUOTA_PROP_BINARY_PREVIEW_MAX_LENGTH), + WebSQLConstants.BINARY_PREVIEW_MAX_LENGTH); byte[] inlineValue = binaryValue; if (inlineValue.length > binaryPreviewMaxLength) { inlineValue = Arrays.copyOf(inlineValue, textPreviewMaxLength); @@ -207,9 +220,11 @@ private static Object serializeGeometryValue(DBGeometry value) { */ public static Object serializeStringValue(Object value) { int textPreviewMaxLength = CommonUtils.toInt( - CBApplication.getInstance().getAppConfiguration().getResourceQuota( - WebSQLConstants.QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH, - WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH)); + ServletAppUtils.getServletApplication() + .getAppConfiguration() + .getResourceQuota(WebSQLConstants.QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH), + WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH + ); String stringValue = value.toString(); if (stringValue.length() < textPreviewMaxLength) { return value.toString(); @@ -245,4 +260,42 @@ public static Object makePlainCellValue(DBCSession session, DBSTypedObject attri } return value; } + + /** + * Returns fully qualified name for a column. + */ + @NotNull + public static String getColumnName(@NotNull DBDAttributeBinding binding) { + return binding.getFullyQualifiedName(DBPEvaluationContext.UI); + } + + @NotNull + public static WebAsyncTaskInfo createAsyncTaskExecuteSqlQuery( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo contextInfo, + @NotNull String sql, + @Nullable String resultId, + @Nullable WebSQLDataFilter filter, + @Nullable WebDataFormat dataFormat, + boolean readLogs + ) { + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException { + try { + monitor.beginTask("Execute query", 1); + monitor.subTask("Process query " + sql); + WebSQLExecuteInfo executeResults = contextInfo.getProcessor().processQuery( + monitor, contextInfo, sql, resultId, filter, dataFormat, webSession, readLogs); + this.result = executeResults.getStatusMessage(); + this.extendedResults = executeResults; + } catch (Throwable e) { + throw new InvocationTargetException(e); + } finally { + monitor.done(); + } + } + }; + return webSession.createAndRunAsyncTask("SQL execute", runnable); + } } 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..ff79e1cb0f 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -19,8 +19,8 @@ import graphql.schema.DataFetchingEnvironment; import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.WebConnectionInfo; +import io.cloudbeaver.model.app.ServletApplication; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.service.DBWBindingContext; import io.cloudbeaver.service.DBWServiceBindingServlet; import io.cloudbeaver.service.DBWServletContext; @@ -39,7 +39,8 @@ /** * Web service implementation */ -public class WebServiceBindingSQL extends WebServiceBindingBase implements DBWServiceBindingServlet { +public class WebServiceBindingSQL extends WebServiceBindingBase + implements DBWServiceBindingServlet { public WebServiceBindingSQL() { super(DBWServiceSQL.class, new WebServiceSQL(), "schema/service.sql.graphqls"); @@ -131,12 +132,30 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getSQLContext(env), env.getArgument("resultId")); }) - .dataFetcher("readLobValue", env -> - getService(env).readLobValue( - getSQLContext(env), - env.getArgument("resultsId"), - env.getArgument("lobColumnIndex"), - getResultsRow(env, "row"))) + .dataFetcher("readLobValue", env -> // deprecated + getService(env).readLobValue( + getSQLContext(env), + env.getArgument("resultsId"), + env.getArgument("lobColumnIndex"), + getResultsRow(env, "row").get(0))) + .dataFetcher("sqlReadLobValue", env -> + getService(env).readLobValue( + getSQLContext(env), + env.getArgument("resultsId"), + env.getArgument("lobColumnIndex"), + new WebSQLResultsRow(env.getArgument("row")))) + .dataFetcher("sqlReadStringValue", env -> + getService(env).getCellValue( + getSQLContext(env), + env.getArgument("resultsId"), + env.getArgument("columnIndex"), + new WebSQLResultsRow(env.getArgument("row")))) + .dataFetcher("sqlGetDynamicTrace", env -> + getService(env).readDynamicTrace( + getWebSession(env), + getSQLContext(env), + env.getArgument("resultsId") + )) .dataFetcher("updateResultsDataBatch", env -> getService(env).updateResultsDataBatch( getSQLContext(env), @@ -145,6 +164,15 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getResultsRow(env, "deletedRows"), getResultsRow(env, "addedRows"), getDataFormat(env))) + .dataFetcher("asyncUpdateResultsDataBatch", env -> + getService(env).asyncUpdateResultsDataBatch( + getWebSession(env), + getSQLContext(env), + env.getArgument("resultsId"), + getResultsRow(env, "updatedRows"), + getResultsRow(env, "deletedRows"), + getResultsRow(env, "addedRows"), + getDataFormat(env))) .dataFetcher("updateResultsDataBatchScript", env -> getService(env).updateResultsDataBatchScript( getSQLContext(env), @@ -156,13 +184,16 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { .dataFetcher("asyncSqlExecuteQuery", env -> getService(env).asyncExecuteQuery( + getWebSession(env), + getProjectReference(env), getSQLContext(env), env.getArgument("sql"), env.getArgument("resultId"), getDataFilter(env), getDataFormat(env), - CommonUtils.toBoolean(env.getArgument("readLogs")), - getWebSession(env))) + CommonUtils.toBoolean(env.getArgument("readLogs")) + ) + ) .dataFetcher("asyncReadDataFromContainer", env -> getService(env).asyncReadDataFromContainer( getSQLContext(env), @@ -184,11 +215,43 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { .dataFetcher("asyncSqlExplainExecutionPlanResult", env -> getService(env).asyncSqlExplainExecutionPlanResult( getWebSession(env), env.getArgument("taskId") + )) + .dataFetcher("asyncSqlRowDataCount", env -> + getService(env).getRowDataCount( + getWebSession(env), + getSQLContext(env), + env.getArgument("resultsId") + )) + .dataFetcher("asyncSqlRowDataCountResult", env -> + getService(env).getRowDataCountResult( + getWebSession(env), + env.getArgument("taskId") + )) + .dataFetcher("asyncSqlSetAutoCommit", env -> + getService(env).asyncSqlSetAutoCommit( + getWebSession(env), + getSQLContext(env), + env.getArgument("autoCommit") + )) + .dataFetcher("asyncSqlCommitTransaction", env -> + getService(env).asyncSqlCommitTransaction( + getWebSession(env), + getSQLContext(env) + )) + .dataFetcher("getTransactionLogInfo", env -> + getService(env).getTransactionLogInfo( + getWebSession(env), + getSQLContext(env) + )) + .dataFetcher("asyncSqlRollbackTransaction", env -> + getService(env).asyncSqlRollbackTransaction( + getWebSession(env), + getSQLContext(env) )); } @NotNull - private WebDataFormat getDataFormat(DataFetchingEnvironment env) { + public static WebDataFormat getDataFormat(DataFetchingEnvironment env) { String dataFormat = env.getArgument("dataFormat"); return CommonUtils.valueOf(WebDataFormat.class, dataFormat, WebDataFormat.resultset); } @@ -249,12 +312,22 @@ public static WebSQLContextInfo getSQLContext(WebSQLProcessor processor, String } @Override - public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException { + public void addServlets(ServletApplication application, DBWServletContext servletContext) throws DBException { servletContext.addServlet( "sqlResultValueViewer", new WebSQLResultServlet(application, getServiceImpl()), application.getServicesURI() + "sql-result-value/*" ); + servletContext.addServlet( + "sqlUploadFile", + new WebSQLFileLoaderServlet(application), + application.getServicesURI() + "resultset/blob/*" + ); + } + + @Override + public boolean isApplicable(ServletApplication application) { + return application.isMultiuser(); } private static class WebSQLConfiguration { @@ -300,8 +373,7 @@ public WebSQLConfiguration dispose() { /////////////////////////////////////// // Helpers - - private static WebSQLDataFilter getDataFilter(DataFetchingEnvironment env) { + public static WebSQLDataFilter getDataFilter(DataFetchingEnvironment env) { Map filterProps = env.getArgument("filter"); return filterProps == null ? null : new WebSQLDataFilter(filterProps); } 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..e9615cbc5e 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -17,9 +17,11 @@ package io.cloudbeaver.service.sql.impl; +import io.cloudbeaver.DBWConstants; import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.WebConnectionInfo; +import io.cloudbeaver.model.WebTransactionLogInfo; import io.cloudbeaver.model.session.WebAsyncTaskProcessor; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.WebServiceBindingBase; @@ -31,16 +33,21 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPDataSource; import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.data.DBDAttributeBinding; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.exec.DBCLogicalOperator; import org.jkiss.dbeaver.model.exec.DBExecUtils; +import org.jkiss.dbeaver.model.exec.trace.DBCTrace; +import org.jkiss.dbeaver.model.exec.trace.DBCTraceDynamic; +import org.jkiss.dbeaver.model.exec.trace.DBCTraceProperty; import org.jkiss.dbeaver.model.impl.sql.BasicSQLDialect; +import org.jkiss.dbeaver.model.navigator.DBNModel; import org.jkiss.dbeaver.model.navigator.DBNNode; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.sql.*; +import org.jkiss.dbeaver.model.sql.completion.CompletionProposalBase; import org.jkiss.dbeaver.model.sql.completion.SQLCompletionAnalyzer; -import org.jkiss.dbeaver.model.sql.completion.SQLCompletionProposalBase; import org.jkiss.dbeaver.model.sql.completion.SQLCompletionRequest; import org.jkiss.dbeaver.model.sql.format.SQLFormatUtils; import org.jkiss.dbeaver.model.sql.generator.SQLGenerator; @@ -48,9 +55,12 @@ import org.jkiss.dbeaver.model.sql.parser.SQLScriptParser; import org.jkiss.dbeaver.model.sql.registry.SQLGeneratorConfigurationRegistry; import org.jkiss.dbeaver.model.sql.registry.SQLGeneratorDescriptor; +import org.jkiss.dbeaver.model.sql.semantics.completion.SQLCompletionProposalComparator; +import org.jkiss.dbeaver.model.sql.semantics.completion.SQLQueryCompletionAnalyzer; import org.jkiss.dbeaver.model.struct.DBSDataContainer; import org.jkiss.dbeaver.model.struct.DBSObject; import org.jkiss.dbeaver.model.struct.DBSWrapper; +import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.utils.RuntimeUtils; import org.jkiss.utils.CommonUtils; @@ -67,6 +77,7 @@ public class WebServiceSQL implements DBWServiceSQL { private static final Log log = Log.getLog(WebServiceSQL.class); + public static final String DEFAULT_ENGINE_COMPLETION = "DEFAULT"; @Override public WebSQLContextInfo[] listContexts( @@ -80,10 +91,10 @@ public WebSQLContextInfo[] listContexts( WebConnectionInfo webConnection = WebServiceBindingBase.getWebConnection(session, projectId, connectionId); conToRead.add(webConnection); } else { - conToRead.addAll(session.getConnections()); + conToRead.addAll(session.getAccessibleProjects().stream().flatMap(p -> p.getConnections().stream()).toList()); } - List contexts = new ArrayList<>(); + List contexts = new ArrayList<>(); for (WebConnectionInfo con : conToRead) { WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(con, false); if (sqlProcessor != null) { @@ -136,14 +147,10 @@ public WebSQLCompletionProposal[] getCompletionProposals( { try { DBPDataSource dataSource = sqlContext.getProcessor().getConnection().getDataSourceContainer().getDataSource(); - Document document = new Document(); document.set(query); - WebSQLCompletionContext completionContext = new WebSQLCompletionContext(sqlContext); - SQLScriptElement activeQuery; - if (position != null) { SQLParserContext parserContext = new SQLParserContext( sqlContext.getProcessor().getConnection().getDataSource(), @@ -155,7 +162,6 @@ public WebSQLCompletionProposal[] getCompletionProposals( activeQuery = new SQLQuery(dataSource, query); } - SQLCompletionRequest request = new SQLCompletionRequest( completionContext, document, @@ -164,11 +170,29 @@ public WebSQLCompletionProposal[] getCompletionProposals( CommonUtils.getBoolean(simpleMode, false) ); - SQLCompletionAnalyzer analyzer = new SQLCompletionAnalyzer(request); - analyzer.setCheckNavigatorNodes(false); - analyzer.runAnalyzer(sqlContext.getProcessor().getWebSession().getProgressMonitor()); - List proposals = analyzer.getProposals(); - if (maxResults == null) maxResults = 200; + List proposals = new ArrayList<>(); + WebSession webSession = sqlContext.getWebSession(); + boolean useDefaultCompletionEngine = DEFAULT_ENGINE_COMPLETION.equalsIgnoreCase(webSession.getUserPreferenceStore() + .getString(SQLModelPreferences.AUTOCOMPLETION_MODE)); + + if (!useDefaultCompletionEngine) { + SQLQueryCompletionAnalyzer analyzer = new SQLQueryCompletionAnalyzer( + m -> WebSQLCompletionContextScriptParser.obtainCompletionContext( + webSession, query, position, request), + request, + request::getDocumentOffset + ); + analyzer.run(webSession.getProgressMonitor()); + proposals.addAll(analyzer.getResult()); + } else { + SQLCompletionAnalyzer analyzer = new SQLCompletionAnalyzer(request); + analyzer.setCheckNavigatorNodes(false); + analyzer.runAnalyzer(sqlContext.getProcessor().getWebSession().getProgressMonitor()); + proposals.addAll(analyzer.getProposals()); + } + if (maxResults == null) { + maxResults = 200; + } if (proposals.size() > maxResults) { proposals = proposals.subList(0, maxResults); } @@ -177,8 +201,13 @@ public WebSQLCompletionProposal[] getCompletionProposals( for (int i = 0; i < proposals.size(); i++) { result[i] = new WebSQLCompletionProposal(proposals.get(i)); } + SQLCompletionProposalComparator sqlCompletionProposalComparator = new SQLCompletionProposalComparator( + completionContext.isSortAlphabetically(), + completionContext.isSearchInsideNames() + ); + Arrays.sort(result, (o1, o2) -> sqlCompletionProposalComparator.compare(o1.getProposal(), o2.getProposal())); return result; - } catch (DBException e) { + } catch (Exception e) { throw new DBWebException("Error processing SQL proposals", e); } } @@ -237,8 +266,9 @@ public String generateEntityQuery(@NotNull WebSession session, @NotNull String g private List getObjectListFromNodeIds(@NotNull WebSession session, @NotNull List nodePathList) throws DBWebException { try { List objectList = new ArrayList<>(nodePathList.size()); + DBNModel navigatorModel = session.getNavigatorModelOrThrow(); for (String nodePath : nodePathList) { - DBNNode node = session.getNavigatorModel().getNodeByPath(session.getProgressMonitor(), nodePath); + DBNNode node = navigatorModel.getNodeByPath(session.getProgressMonitor(), nodePath); if (node == null) { throw new DBException("Node '" + nodePath + "' not found"); } @@ -288,6 +318,47 @@ public Boolean closeResult(@NotNull WebSQLContextInfo sqlContext, @NotNull Strin return true; } + @NotNull + @Override + public WebAsyncTaskInfo asyncUpdateResultsDataBatch( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @Nullable List updatedRows, + @Nullable List deletedRows, + @Nullable List addedRows, + @Nullable WebDataFormat dataFormat + ) throws DBWebException { + if (DBWorkbench.isDistributed() && !webSession.hasPermission(DBWConstants.PERMISSION_SQL_RESULT_UPDATE)) { + throw new DBWebException("Permission denied"); + } + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException { + try { + monitor.beginTask("Update result set data", 1); + monitor.subTask("Update result set data from id " + resultsId); + WebSQLExecuteInfo executeResults = updateResultsDataBatch( + monitor, + contextInfo, + resultsId, + updatedRows, + deletedRows, + addedRows, + dataFormat + ); + this.result = executeResults.getStatusMessage(); + this.extendedResults = executeResults; + } catch (Throwable e) { + throw new InvocationTargetException(e); + } finally { + monitor.done(); + } + } + }; + return webSession.createAndRunAsyncTask("Updating result set data from " + resultsId, runnable); + } + @Override public WebSQLExecuteInfo updateResultsDataBatch( @NotNull WebSQLContextInfo contextInfo, @@ -295,54 +366,108 @@ public WebSQLExecuteInfo updateResultsDataBatch( @Nullable List updatedRows, @Nullable List deletedRows, @Nullable List addedRows, - @Nullable WebDataFormat dataFormat) throws DBWebException - { + @Nullable WebDataFormat dataFormat + ) throws DBWebException { + if (DBWorkbench.isDistributed() && !contextInfo.getWebSession().hasPermission(DBWConstants.PERMISSION_SQL_RESULT_UPDATE)) { + throw new DBWebException("Permission denied"); + } try { - WebSQLExecuteInfo[] result = new WebSQLExecuteInfo[1]; - - DBExecUtils.tryExecuteRecover( - contextInfo.getProcessor().getWebSession().getProgressMonitor(), - contextInfo.getProcessor().getConnection().getDataSource(), - monitor -> { - try { - result[0] = contextInfo.getProcessor().updateResultsDataBatch( - monitor, contextInfo, resultsId, updatedRows, deletedRows, addedRows, dataFormat); - } catch (Exception e) { - throw new InvocationTargetException(e); - } - } + return updateResultsDataBatch( + contextInfo.getWebSession().getProgressMonitor(), + contextInfo, + resultsId, + updatedRows, + deletedRows, + addedRows, + dataFormat ); - return result[0]; } catch (DBException e) { throw new DBWebException("Error updating resultset data", e); } } + private WebSQLExecuteInfo updateResultsDataBatch( + @NotNull DBRProgressMonitor monitor, + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @Nullable List updatedRows, + @Nullable List deletedRows, + @Nullable List addedRows, + @Nullable WebDataFormat dataFormat + ) throws DBException { + WebSQLExecuteInfo[] result = new WebSQLExecuteInfo[1]; + + DBExecUtils.tryExecuteRecover( + monitor, + contextInfo.getProcessor().getConnection().getDataSource(), + monitor1 -> { + try { + result[0] = contextInfo.getProcessor().updateResultsDataBatch( + monitor1, contextInfo, resultsId, updatedRows, deletedRows, addedRows, dataFormat); + } catch (Exception e) { + throw new InvocationTargetException(e); + } + } + ); + return result[0]; + } + + @FunctionalInterface + private interface ThrowableFunction { + R apply(T obj) throws Exception; + } + @Override public String readLobValue( @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, @NotNull Integer lobColumnIndex, - @Nullable List row) throws DBWebException + @NotNull WebSQLResultsRow row) throws DBWebException { + ThrowableFunction function = monitor -> contextInfo.getProcessor().readLobValue( + monitor, contextInfo, resultsId, lobColumnIndex, row); + return readValue(function, contextInfo.getProcessor()); + } + + @NotNull + @Override + public String getCellValue( + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @NotNull Integer lobColumnIndex, + @NotNull WebSQLResultsRow row + ) throws DBWebException { + if (row == null) { + throw new DBWebException("Results row is not found"); + } + WebSQLProcessor processor = contextInfo.getProcessor(); + ThrowableFunction function = monitor -> processor.readStringValue( + monitor, contextInfo, resultsId, lobColumnIndex, row); + return readValue(function, processor); + } + + @NotNull + private String readValue( + @NotNull ThrowableFunction function, + @NotNull WebSQLProcessor processor + ) throws DBWebException { try { var result = new StringBuilder(); DBExecUtils.tryExecuteRecover( - contextInfo.getProcessor().getWebSession().getProgressMonitor(), - contextInfo.getProcessor().getConnection().getDataSource(), - monitor -> { - try { - result.append(contextInfo.getProcessor().readLobValue( - monitor, contextInfo, resultsId, lobColumnIndex, row.get(0))); - } catch (Exception e) { - throw new InvocationTargetException(e); - } + processor.getWebSession().getProgressMonitor(), + processor.getConnection().getDataSource(), + monitor -> { + try { + result.append(function.apply(monitor)); + } catch (Exception e) { + throw new InvocationTargetException(e); } + } ); return result.toString(); } catch (DBException e) { - throw new DBWebException("Error reading LOB value ", e); + throw new DBWebException("Error reading value ", e); } } @@ -358,33 +483,29 @@ public String updateResultsDataBatchScript(@NotNull WebSQLContextInfo contextInf } @NotNull + @Override public WebAsyncTaskInfo asyncExecuteQuery( + @NotNull WebSession webSession, + @NotNull String projectId, @NotNull WebSQLContextInfo contextInfo, @NotNull String sql, @Nullable String resultId, @Nullable WebSQLDataFilter filter, @Nullable WebDataFormat dataFormat, - boolean readLogs, - @NotNull WebSession webSession) - { - WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { - @Override - public void run(DBRProgressMonitor monitor) throws InvocationTargetException { - try { - monitor.beginTask("Execute query", 1); - monitor.subTask("Process query " + sql); - WebSQLExecuteInfo executeResults = contextInfo.getProcessor().processQuery( - monitor, contextInfo, sql, resultId, filter, dataFormat, webSession, readLogs); - this.result = executeResults.getStatusMessage(); - this.extendedResults = executeResults; - } catch (Throwable e) { - throw new InvocationTargetException(e); - } finally { - monitor.done(); - } - } - }; - return contextInfo.getProcessor().getWebSession().createAndRunAsyncTask("SQL execute", runnable); + boolean readLogs + ) throws DBException { + if (DBWorkbench.isDistributed() && !webSession.hasPermission(DBWConstants.PERMISSION_SQL_EXECUTE_QUERY)) { + throw new DBWebException("Permission denied"); + } + return WebSQLUtils.createAsyncTaskExecuteSqlQuery( + webSession, + contextInfo, + sql, + resultId, + filter, + dataFormat, + readLogs + ); } @Override @@ -404,7 +525,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, @@ -423,6 +544,21 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException, In return contextInfo.getProcessor().getWebSession().createAndRunAsyncTask("Read data from container " + nodePath, runnable); } + @NotNull + @Override + public List readDynamicTrace( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId + ) throws DBException { + WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); + DBCTrace trace = resultsInfo.getTrace(); + if (trace instanceof DBCTraceDynamic traceDynamic) { + return traceDynamic.getTraceProperties(webSession.getProgressMonitor()); + } + throw new DBWebException("Dynamic trace is not found in provided results info"); + } + @Override public WebSQLExecuteInfo asyncGetQueryResults(@NotNull WebSession webSession, @NotNull String taskId) throws DBWebException { WebAsyncTaskInfo taskStatus = webSession.asyncTaskStatus(taskId, false); @@ -502,13 +638,18 @@ public String generateGroupByQuery( try { WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); + List groupingAttributes = Arrays.stream(resultsInfo.getAttributes()) + .filter(attr -> columnsList.contains(WebSQLUtils.getColumnName(attr))) + .map(SQLGroupingAttribute::makeBound) + .toList(); + var dataSource = contextInfo.getProcessor().getConnection().getDataSource(); var groupingQueryGenerator = new SQLGroupingQueryGenerator( dataSource, resultsInfo.getDataContainer(), getSqlDialectFromConnection(dataSource.getContainer()), contextInfo.getProcessor().getSyntaxManager(), - columnsList, + groupingAttributes, functions == null ? List.of(SQLGroupingQueryGenerator.DEFAULT_FUNCTION) : functions, // backward compatibility CommonUtils.getBoolean(showDuplicatesOnly, false)); return groupingQueryGenerator.generateGroupingQuery(resultsInfo.getQueryText()); @@ -516,4 +657,55 @@ public String generateGroupByQuery( throw new DBWebException("Error on generating GROUP BY query", e); } } + + @Override + public WebAsyncTaskInfo getRowDataCount(@NotNull WebSession webSession, @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId) throws DBWebException { + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + try { + monitor.beginTask("Get row data count", 1); + WebSQLResultsInfo results = contextInfo.getResults(resultsId); + long rowCount = DBUtils.readRowCount(monitor, contextInfo.getProcessor().getExecutionContext(), results.getDataContainer(), null, this); + this.result = "Row data count completed"; + this.extendedResults = rowCount; + } catch (Throwable e) { + throw new InvocationTargetException(e); + } finally { + monitor.done(); + } + } + }; + return contextInfo.getProcessor().getWebSession().createAndRunAsyncTask("SQL result set count rows", runnable); + } + + @Override + @Nullable + public Long getRowDataCountResult(@NotNull WebSession webSession, @NotNull String taskId) throws DBWebException { + WebAsyncTaskInfo taskStatus = webSession.asyncTaskStatus(taskId, false); + if (taskStatus != null) { + return (Long) taskStatus.getExtendedResult(); + } + return null; + } + + @Override + public WebAsyncTaskInfo asyncSqlSetAutoCommit(@NotNull WebSession webSession, @NotNull WebSQLContextInfo contextInfo, boolean autoCommit) throws DBWebException { + return contextInfo.setAutoCommit(autoCommit); + } + + @Override + public WebAsyncTaskInfo asyncSqlRollbackTransaction(@NotNull WebSession webSession, @NotNull WebSQLContextInfo contextInfo) throws DBWebException { + return contextInfo.rollbackTransaction(); + } + + @Override + public WebAsyncTaskInfo asyncSqlCommitTransaction(@NotNull WebSession webSession, @NotNull WebSQLContextInfo contextInfo) { + return contextInfo.commitTransaction(); + } + + @Override + public WebTransactionLogInfo getTransactionLogInfo(@NotNull WebSession webSession, @NotNull WebSQLContextInfo sqlContext) { + return sqlContext.getTransactionLogInfo(); + } } diff --git a/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF index 6cdf1834b2..9003c82a9c 100644 --- a/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF @@ -3,11 +3,10 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Administration Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true -Bundle-Version: 1.0.82.qualifier -Bundle-Release-Date: 20230209 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 1.0.129.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy -Bundle-ClassPath: . -Require-Bundle: io.cloudbeaver.server, +Require-Bundle: io.cloudbeaver.server.ce, org.jkiss.dbeaver.registry Automatic-Module-Name: io.cloudbeaver.service.admin diff --git a/server/bundles/io.cloudbeaver.service.admin/pom.xml b/server/bundles/io.cloudbeaver.service.admin/pom.xml index 5d5784c86c..9534e875bd 100644 --- a/server/bundles/io.cloudbeaver.service.admin/pom.xml +++ b/server/bundles/io.cloudbeaver.service.admin/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.admin - 1.0.82-SNAPSHOT + 1.0.129-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls index 67e8da9fa6..1b1987cd79 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -11,6 +11,11 @@ type AdminConnectionGrantInfo { subjectType: AdminSubjectType! } +type AdminUserTeamGrantInfo @since(version: "24.0.5"){ + userId: ID! + teamRole: String +} + type AdminObjectPermissions { objectId: ID! permissions: [String!]! @@ -43,6 +48,11 @@ type AdminUserInfo { linkedAuthProviders: [String!]! enabled: Boolean! authRole: String + + disableDate: DateTime @since(version: "25.0.2") + disabledBy: String @since(version: "25.0.2") + disableReason: String @since(version: "25.0.2") + } type AdminTeamInfo { @@ -53,6 +63,7 @@ type AdminTeamInfo { metaParameters: Object! grantedUsers: [ID!]! + grantedUsersInfo: [AdminUserTeamGrantInfo!]! @since(version: "24.0.5") grantedConnections: [AdminConnectionGrantInfo!]! teamPermissions: [ID!]! @@ -82,6 +93,8 @@ type AdminAuthProviderConfiguration { signOutLink: String redirectLink: String metadataLink: String + acsLink: String + entityIdLink: String @since(version: "24.2.1") } type WebFeatureSet { @@ -95,7 +108,7 @@ type WebFeatureSet { input ServerConfigInput { serverName: String - serverURL: String + serverURL: String @deprecated adminName: String adminPassword: String @@ -106,12 +119,16 @@ input ServerConfigInput { publicCredentialsSaveEnabled: Boolean adminCredentialsSaveEnabled: Boolean resourceManagerEnabled: Boolean + secretManagerEnabled: Boolean @since(version: "24.3.2") enabledFeatures: [ID!] enabledAuthProviders: [ID!] disabledDrivers: [ID!] sessionExpireTime: Int + forceHttps: Boolean @since(version: "25.1.3") + supportedHosts: [String!] @since(version: "25.1.3") + bindSessionToIp: String @since(version: "25.1.4") } input AdminUserFilterInput { @@ -122,55 +139,94 @@ input AdminUserFilterInput { extend type Query { #### Users and teams + "Returns information about user by userId" adminUserInfo(userId: ID!): AdminUserInfo! + "Returns all users with pagination and filter" listUsers(page: PageInput!, filter: AdminUserFilterInput!): [AdminUserInfo!]! + "Returns information about team by teamId. If teamId is not provided, returns information about all teams" listTeams(teamId: ID): [AdminTeamInfo!]! + "Returns all permissions" listPermissions: [AdminPermissionInfo!]! + "Returns all auth roles based on the license" listAuthRoles: [String!]! + "Returns all team roles" + listTeamRoles: [String!]! + "Returns teams meta parameters for displaying in the UI" listTeamMetaParameters: [ObjectPropertyInfo!]! + "Creates a new user with the specified userId" createUser(userId: ID!, enabled: Boolean!, authRole: String): AdminUserInfo! + "Deletes user by userId" deleteUser(userId: ID!): Boolean + "Creates a new team with the specified teamId" createTeam(teamId: ID!, teamName: String, description: String): AdminTeamInfo! + "Updates team information by teamId" updateTeam(teamId: ID!, teamName: String, description: String): AdminTeamInfo! + "Deletes team by teamId" deleteTeam(teamId: ID!, force: Boolean): Boolean + "Grants user to team with the specified userId and teamId" grantUserTeam(userId: ID!, teamId: ID!): Boolean + "Revokes user from team with the specified userId and teamId" revokeUserTeam(userId: ID!, teamId: ID!): Boolean + "Sets permissions to the subject (user or team) with the specified subjectId" setSubjectPermissions(subjectId: ID!, permissions: [ID!]!): [AdminPermissionInfo!]! + "Sets user credentials for the specified userId and providerId" setUserCredentials(userId: ID!, providerId: ID!, credentials: Object!): Boolean + "Deletes user credentials for the specified userId and providerId" deleteUserCredentials(userId: ID!, providerId: ID!): Boolean + "Enables or disables user by userId" enableUser(userId: ID!, enabled: Boolean!): Boolean + "Sets user auth role for the specified userId" setUserAuthRole(userId: ID!, authRole: String): Boolean + "Sets user team role for the specified userId and teamId" + setUserTeamRole(userId: ID!, teamId: ID!, teamRole: String): Boolean @since(version: "24.0.5") + #### Connection management - # All connection configurations + "Finds available connections by host names" searchConnections( hostNames: [String!]! ): [AdminConnectionSearchInfo!]! # Permissions + "Returns all subjects (users and teams) that have access to the specified connection" getConnectionSubjectAccess(projectId: ID!, connectionId: ID): [AdminConnectionGrantInfo!]! - setConnectionSubjectAccess(projectId: ID!, connectionId: ID!, subjects: [ID!]!): Boolean + "Sets access to the connection for the specified subjects (users and teams)" + setConnectionSubjectAccess(projectId: ID!, connectionId: ID!, subjects: [ID!]!): Boolean @deprecated(reason: "use addConnectionsAccess (23.2.2)") + + "Sets access to the connections for the specified subjects (users and teams)" + addConnectionsAccess(projectId: ID!, connectionIds: [ID!]!, subjects: [ID!]!): Boolean @since(version: "23.2.2") + + "Deletes access to the connections for the specified subjects (users and teams)" + deleteConnectionsAccess(projectId: ID!, connectionIds: [ID!]!, subjects: [ID!]!): Boolean @since(version: "23.2.2") + + "Returns all connections that the subject (user or team) has access to" getSubjectConnectionAccess(subjectId: ID!): [AdminConnectionGrantInfo!]! - setSubjectConnectionAccess(subjectId: ID!, connections: [ID!]!): Boolean + + "Sets access for a subject (user or team) to the specified connections" + setSubjectConnectionAccess(subjectId: ID!, connections: [ID!]!): Boolean @deprecated(reason: "23.2.2") #### Feature sets + "Returns all available feature sets that can be enabled or disabled" listFeatureSets: [WebFeatureSet!]! #### Auth providers and configurations + "Returns all properties of the auth provider with the specified providerId" listAuthProviderConfigurationParameters(providerId: ID!): [ObjectPropertyInfo!]! + "Returns all auth provider configurations for the specified providerId. If providerId is not provided, returns all configurations" listAuthProviderConfigurations(providerId: ID): [AdminAuthProviderConfiguration!]! + "Saves auth provider configuration with the specified parameters" saveAuthProviderConfiguration( providerId: ID!, id: ID!, @@ -178,24 +234,37 @@ extend type Query { disabled: Boolean, iconURL: String description: String - parameters: Object): AdminAuthProviderConfiguration! + parameters: Object + ): AdminAuthProviderConfiguration! + + "Deletes auth provider configuration by id" deleteAuthProviderConfiguration(id: ID!): Boolean! #### User profile + "Not implemented yet" saveUserMetaParameter(id: ID!, displayName: String!, description: String, required: Boolean!): ObjectPropertyInfo! + "Not implemented yet" deleteUserMetaParameter(id: ID!): Boolean! + "Sets user meta parameters values for the specified userId" setUserMetaParameterValues(userId: ID!, parameters: Object!): Boolean! + "Sets team meta parameters values for the specified teamId" setTeamMetaParameterValues(teamId: ID!, parameters: Object!): Boolean! #### Global configuration + "Saves server configuration" configureServer(configuration: ServerConfigInput!): Boolean! - # Changes default navigator settings + "Changes default navigator settings" setDefaultNavigatorSettings( settings: NavigatorSettingsInput!): Boolean! } + +extend type Mutation { + "Updates product configuration" + adminUpdateProductConfiguration(configuration: Object!): Boolean! @since(version: "23.3.4") +} diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminConnectionSearchInfo.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminConnectionSearchInfo.java index 6eec009955..5cec480597 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminConnectionSearchInfo.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminConnectionSearchInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminOriginInfo.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminOriginInfo.java index 9ed3c2086d..e6588ea85f 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminOriginInfo.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminOriginInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java index 868b24c213..e46498445a 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,8 +16,11 @@ */ package io.cloudbeaver.service.admin; -import io.cloudbeaver.server.CBAppConfig; +import io.cloudbeaver.model.config.CBAppConfig; import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBConstants; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.data.json.JSONUtils; import java.util.Arrays; @@ -28,6 +31,7 @@ * Server configuration for admin API */ public class AdminServerConfig { + private static final Log log = Log.getLog(AdminServerConfig.class); private String serverName; private String serverURL; @@ -37,6 +41,7 @@ public class AdminServerConfig { private final boolean anonymousAccessEnabled; private final boolean resourceManagerEnabled; + private final boolean secretManagerEnabled; private final boolean customConnectionsEnabled; private final boolean publicCredentialsSaveEnabled; private final boolean adminCredentialsSaveEnabled; @@ -44,8 +49,14 @@ public class AdminServerConfig { private final List enabledAuthProviders; private final String[] enabledDrivers; private final String[] disabledDrivers; + @Nullable + private final Boolean forceHttps; + @Nullable + private final List supportedHosts; private long sessionExpireTime; + @Nullable + private String bindSessionToIp; public AdminServerConfig(Map params) { this.serverName = JSONUtils.getString(params, "serverName"); @@ -56,9 +67,18 @@ public AdminServerConfig(Map params) { CBAppConfig appConfig = CBApplication.getInstance().getAppConfiguration(); this.anonymousAccessEnabled = JSONUtils.getBoolean(params, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled()); this.customConnectionsEnabled = JSONUtils.getBoolean(params, "customConnectionsEnabled", appConfig.isSupportsCustomConnections()); - this.publicCredentialsSaveEnabled = JSONUtils.getBoolean(params, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled()); - this.adminCredentialsSaveEnabled = JSONUtils.getBoolean(params, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled()); + this.publicCredentialsSaveEnabled = JSONUtils.getBoolean( + params, + "publicCredentialsSaveEnabled", + appConfig.isPublicCredentialsSaveEnabled() + ); + this.adminCredentialsSaveEnabled = JSONUtils.getBoolean( + params, + "adminCredentialsSaveEnabled", + appConfig.isAdminCredentialsSaveEnabled() + ); this.resourceManagerEnabled = JSONUtils.getBoolean(params, "resourceManagerEnabled", appConfig.isResourceManagerEnabled()); + this.secretManagerEnabled = JSONUtils.getBoolean(params, "secretManagerEnabled", appConfig.isSecretManagerEnabled()); if (params.containsKey("enabledFeatures")) { this.enabledFeatures = JSONUtils.getStringList(params, "enabledFeatures"); @@ -85,6 +105,24 @@ public AdminServerConfig(Map params) { } else { this.disabledDrivers = appConfig.getDisabledDrivers(); } + + if (params.containsKey(CBConstants.PARAM_FORCE_HTTPS)) { + this.forceHttps = JSONUtils.getBoolean(params, CBConstants.PARAM_FORCE_HTTPS); + } else { + this.forceHttps = null; + } + + if (params.containsKey(CBConstants.PARAM_SUPPORTED_HOSTS)) { + this.supportedHosts = JSONUtils.getStringList(params, CBConstants.PARAM_SUPPORTED_HOSTS); + } else { + this.supportedHosts = null; + } + + if (params.containsKey("bindSessionToIp")) { + this.bindSessionToIp = JSONUtils.getString(params, "bindSessionToIp"); + } else { + this.bindSessionToIp = CBConstants.BIND_SESSION_DISABLE; + } } public String getServerName() { @@ -162,4 +200,23 @@ public String[] getDisabledDrivers() { public boolean isResourceManagerEnabled() { return resourceManagerEnabled; } + + public boolean isSecretManagerEnabled() { + return secretManagerEnabled; + } + + @Nullable + public List getSupportedHosts() { + return supportedHosts; + } + + @Nullable + public Boolean getForceHttps() { + return forceHttps; + } + + @Nullable + public String getBindSessionToIp() { + return bindSessionToIp; + } } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminTeamInfo.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminTeamInfo.java index 4769c018c0..6951cbf6b0 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminTeamInfo.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminTeamInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -21,6 +21,7 @@ import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.security.SMDataSourceGrant; import org.jkiss.dbeaver.model.security.SMObjectType; +import org.jkiss.dbeaver.model.security.SMTeamMemberInfo; import org.jkiss.dbeaver.model.security.user.SMTeam; import java.util.ArrayList; @@ -88,4 +89,8 @@ public String[] getGrantedUsers() throws DBException { return session.getAdminSecurityController().getTeamMembers(getTeamId()); } + @Property + public List getGrantedUsersInfo() throws DBException { + return session.getAdminSecurityController().getTeamMembersInfo(getTeamId()); + } } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfo.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfo.java index 37011cd05c..ad67175441 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfo.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -22,6 +22,8 @@ import io.cloudbeaver.model.user.WebUserOriginInfo; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; +import io.cloudbeaver.utils.CBModelConstants; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.meta.Property; @@ -125,4 +127,19 @@ private String[] getUserLinkedProviders() throws DBWebException { return userLinkedProviders; } + @Nullable + public String getDisableDate() { + return user.getDisableDate() == null ? null : CBModelConstants.ISO_DATE_FORMAT.format(user.getDisableDate()); + } + + + @Nullable + public String getDisabledBy() { + return user.getDisabledBy(); + } + + @Nullable + public String getDisableReason() { + return user.getDisableReason(); + } } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfoFilter.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfoFilter.java index 52e1d90769..5cbf010c6a 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfoFilter.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminUserInfoFilter.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java index 05bc52993e..9c0485487a 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -24,6 +24,7 @@ import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebAuthProviderConfiguration; import io.cloudbeaver.service.DBWService; +import jakarta.servlet.http.HttpServletRequest; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; @@ -69,6 +70,9 @@ AdminUserInfo createUser( @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) List listAuthRoles(); + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + List listTeamRoles(); + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) boolean deleteUser(@NotNull WebSession webSession, String userName) throws DBWebException; @@ -116,10 +120,11 @@ AdminUserInfo createUser( @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) List listAuthProviderConfigurationParameters(@NotNull WebSession webSession, @NotNull String providerId) throws DBWebException; @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) - List listAuthProviderConfigurations(@NotNull WebSession webSession, @Nullable String providerId) throws DBWebException; + List listAuthProviderConfigurations(@NotNull HttpServletRequest request, @NotNull WebSession webSession, @Nullable String providerId) throws DBWebException; @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) WebAuthProviderConfiguration saveAuthProviderConfiguration( + @NotNull HttpServletRequest request, @NotNull WebSession webSession, @NotNull String providerId, @NotNull String id, @@ -138,6 +143,9 @@ WebAuthProviderConfiguration saveAuthProviderConfiguration( @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) boolean setDefaultNavigatorSettings(WebSession webSession, DBNBrowseSettings settings) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + boolean updateProductConfiguration(WebSession webSession, Map productConfiguration) throws DBWebException; + //////////////////////////////////////////////////////////////////// // Permissions @@ -147,6 +155,7 @@ SMDataSourceGrant[] getConnectionSubjectAccess( @Nullable String projectId, String connectionId) throws DBWebException; + @Deprecated @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) boolean setConnectionSubjectAccess( @NotNull WebSession webSession, @@ -154,6 +163,22 @@ boolean setConnectionSubjectAccess( @NotNull String connectionId, @NotNull List subjects) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + boolean addConnectionsAccess( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull List connectionIds, + @NotNull List subjects + ) throws DBWebException; + + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + boolean deleteConnectionsAccess( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull List connectionIds, + @NotNull List subjects + ) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) SMDataSourceGrant[] getSubjectConnectionAccess(@NotNull WebSession webSession, @NotNull String subjectId) throws DBWebException; @@ -162,6 +187,7 @@ boolean setSubjectConnectionAccess(@NotNull WebSession webSession, @NotNull Stri DBWebException; //////////////////////////////////////////////////////////////////// + // User meta parameters @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) @@ -170,15 +196,21 @@ WebPropertyInfo saveUserMetaParameter(WebSession webSession, String id, String d @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) Boolean deleteUserMetaParameter(WebSession webSession, String id) throws DBWebException; - @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) Boolean setUserMetaParameterValues(WebSession webSession, String userId, Map parameters) throws DBWebException; @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) Boolean setTeamMetaParameterValues(WebSession webSession, String teamId, Map parameters) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) Boolean enableUser(WebSession webSession, String userId, Boolean enabled) throws DBWebException; @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) Boolean setUserAuthRole(WebSession webSession, String userId, String authRole) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + Boolean setUserTeamRole( + @NotNull WebSession webSession, @NotNull String userId, + @NotNull String teamId, @Nullable String teamRole + ) throws DBWebException; + } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java index 551c292581..06ef5dc632 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -19,6 +19,7 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.graphql.GraphQLEndpoint; import io.cloudbeaver.service.DBWBindingContext; import io.cloudbeaver.service.DBWServiceBindingServlet; import io.cloudbeaver.service.DBWServletContext; @@ -60,6 +61,8 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env -> getService(env).listPermissions(getWebSession(env))) .dataFetcher("listAuthRoles", env -> getService(env).listAuthRoles()) + .dataFetcher("listTeamRoles", + env -> getService(env).listTeamRoles()) .dataFetcher("listTeamMetaParameters", env -> getService(env).listTeamMetaParameters(getWebSession(env))) .dataFetcher("createUser", @@ -95,27 +98,49 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env -> getService(env).revokeUserTeam(getWebSession(env), env.getArgument("userId"), env.getArgument("teamId"))) .dataFetcher("setSubjectPermissions", env -> getService(env).setSubjectPermissions(getWebSession(env), env.getArgument("subjectId"), env.getArgument("permissions"))) - .dataFetcher("setUserCredentials", - env -> getService(env).setUserCredentials(getWebSession(env), env.getArgument("userId"), env.getArgument("providerId"), env.getArgument("credentials"))) - .dataFetcher("deleteUserCredentials", - env -> getService(env).deleteUserCredentials(getWebSession(env), env.getArgument("userId"), env.getArgument("providerId"))) - .dataFetcher("enableUser", - env -> getService(env).enableUser(getWebSession(env), env.getArgument("userId"), env.getArgument("enabled"))) - .dataFetcher("setUserAuthRole", - env -> getService(env).setUserAuthRole(getWebSession(env), env.getArgument("userId"), env.getArgument("authRole"))) - .dataFetcher("searchConnections", env -> getService(env).searchConnections(getWebSession(env), env.getArgument("hostNames"))) - - .dataFetcher("getConnectionSubjectAccess", - env -> getService(env).getConnectionSubjectAccess( - getWebSession(env), - getProjectReference(env), - env.getArgument("connectionId"))) - .dataFetcher("setConnectionSubjectAccess", - env -> getService(env).setConnectionSubjectAccess( - getWebSession(env), - getProjectReference(env), - env.getArgument("connectionId"), - env.getArgument("subjects"))) + .dataFetcher("setUserCredentials", + env -> getService(env).setUserCredentials(getWebSession(env), + env.getArgument("userId"), + env.getArgument("providerId"), + env.getArgument("credentials"))) + .dataFetcher("deleteUserCredentials", + env -> getService(env).deleteUserCredentials(getWebSession(env), env.getArgument("userId"), env.getArgument("providerId"))) + .dataFetcher("enableUser", + env -> getService(env).enableUser(getWebSession(env), env.getArgument("userId"), env.getArgument("enabled"))) + .dataFetcher("setUserAuthRole", + env -> getService(env).setUserAuthRole(getWebSession(env), env.getArgument("userId"), env.getArgument("authRole"))) + .dataFetcher("setUserTeamRole", + env -> getService(env).setUserTeamRole( + getWebSession(env), + env.getArgument("userId"), + env.getArgument("teamId"), + env.getArgument("teamRole") + ) + ) + .dataFetcher("searchConnections", env -> getService(env).searchConnections(getWebSession(env), env.getArgument("hostNames"))) + .dataFetcher("getConnectionSubjectAccess", + env -> getService(env).getConnectionSubjectAccess( + getWebSession(env), + getProjectReference(env), + env.getArgument("connectionId"))) + .dataFetcher("setConnectionSubjectAccess", + env -> getService(env).setConnectionSubjectAccess( + getWebSession(env), + getProjectReference(env), + env.getArgument("connectionId"), + env.getArgument("subjects"))) + .dataFetcher("addConnectionsAccess", + env -> getService(env).addConnectionsAccess( + getWebSession(env), + getProjectReference(env), + env.getArgument("connectionIds"), + env.getArgument("subjects"))) + .dataFetcher("deleteConnectionsAccess", + env -> getService(env).deleteConnectionsAccess( + getWebSession(env), + getProjectReference(env), + env.getArgument("connectionIds"), + env.getArgument("subjects"))) .dataFetcher("getSubjectConnectionAccess", env -> getService(env).getSubjectConnectionAccess(getWebSession(env), env.getArgument("subjectId"))) @@ -128,17 +153,21 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { .dataFetcher("listAuthProviderConfigurationParameters", env -> getService(env).listAuthProviderConfigurationParameters(getWebSession(env), env.getArgument("providerId"))) .dataFetcher("listAuthProviderConfigurations", - env -> getService(env).listAuthProviderConfigurations(getWebSession(env), env.getArgument("providerId"))) + env -> getService(env).listAuthProviderConfigurations( + GraphQLEndpoint.getServletRequestOrThrow(env), + getWebSession(env), env.getArgument("providerId")) + ) .dataFetcher("saveAuthProviderConfiguration", env -> getService(env).saveAuthProviderConfiguration( + GraphQLEndpoint.getServletRequestOrThrow(env), getWebSession(env), env.getArgument("providerId"), env.getArgument("id"), env.getArgument("displayName"), CommonUtils.toBoolean((Boolean)env.getArgument("disabled")), env.getArgument("iconURL"), - env.getArgument("description"), - env.getArgument("parameters"))) + env.getArgument("description"), env.getArgument("parameters") + )) .dataFetcher("deleteAuthProviderConfiguration", env -> getService(env).deleteAuthProviderConfiguration(getWebSession(env), env.getArgument("id"))) @@ -171,10 +200,16 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { .dataFetcher("setDefaultNavigatorSettings", env -> getService(env).setDefaultNavigatorSettings(getWebSession(env), WebServiceUtils.parseNavigatorSettings(env.getArgument("settings")))) ; + model.getMutationType() + .dataFetcher("adminUpdateProductConfiguration", + env -> getService(env).updateProductConfiguration(getWebSession(env), env.getArgument("configuration"))); } @Override public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException { + if(!application.isMultiuser()) { + return; + } servletContext.addServlet("adminLogs", new WebAdminLogsServlet(application), application.getServicesURI() + "logs/*"); } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java index 9eaa454101..0f2df31b78 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,9 +16,12 @@ */ package io.cloudbeaver.service.admin.impl; +import io.cloudbeaver.model.config.CBAppConfig; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.model.utils.ConfigurationUtils; +import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; -import io.cloudbeaver.server.ConfigurationUtils; +import io.cloudbeaver.server.WebAppUtils; import io.cloudbeaver.service.admin.AdminConnectionSearchInfo; import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; @@ -42,14 +45,15 @@ public class ConnectionSearcher implements DBRRunnableWithProgress { private final WebSession webSession; private final String[] hostNames; private final List foundConnections = new ArrayList<>(); - private List availableDrivers = new ArrayList<>(); public ConnectionSearcher(WebSession webSession, String[] hostNames) { this.webSession = webSession; this.hostNames = hostNames; - this.availableDrivers.addAll(CBPlatform.getInstance().getApplicableDrivers()); } + /** + * Returns all found connections in a current machine. + */ public List getFoundConnections() { synchronized (foundConnections) { return new ArrayList<>(foundConnections); @@ -105,7 +109,7 @@ private void searchConnections(DBRProgressMonitor monitor, String hostName, Stri int checkTimeout = 150; Map portCache = new HashMap<>(); - for (DBPDriver driver : availableDrivers) { + for (DBPDriver driver : WebAppUtils.getWebApplication().getDriverRegistry().getApplicableDrivers()) { monitor.subTask("Check '" + driver.getName() + "' on '" + hostName + "'"); if (!CommonUtils.isEmpty(driver.getDefaultPort()) && !isPortInBlockList(CommonUtils.toInt(driver.getDefaultPort()))) { updatePortInfo(portCache, hostName, displayName, driver, checkTimeout); @@ -124,7 +128,12 @@ private void searchConnections(DBRProgressMonitor monitor, String hostName, Stri } private void updatePortInfo(Map portCache, String hostName, String displayName, DBPDriver driver, int timeout) { - if (!ConfigurationUtils.isDriverEnabled(driver)) { + CBAppConfig config = CBApplication.getInstance().getAppConfiguration(); + if (!ConfigurationUtils.isDriverEnabled( + driver, + config.getEnabledDrivers(), + config.getDisabledDrivers()) + ) { return; } int driverPort = CommonUtils.toInt(driver.getDefaultPort()); diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebAdminLogsServlet.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebAdminLogsServlet.java index 749a6d8d00..b25fe8be78 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebAdminLogsServlet.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebAdminLogsServlet.java @@ -12,8 +12,8 @@ import org.jkiss.dbeaver.utils.MimeTypes; import org.jkiss.utils.IOUtils; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; 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 039fed41f4..474d48662c 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -22,17 +22,20 @@ import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.auth.provider.local.LocalAuthProvider; import io.cloudbeaver.model.WebPropertyInfo; +import io.cloudbeaver.model.config.CBAppConfig; +import io.cloudbeaver.model.config.CBServerConfig; import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.user.WebUser; import io.cloudbeaver.registry.*; -import io.cloudbeaver.server.CBAppConfig; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBConstants; -import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.server.WebAppUtils; import io.cloudbeaver.service.DBWServiceServerConfigurator; import io.cloudbeaver.service.admin.*; import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.utils.ServletAppUtils; +import jakarta.servlet.http.HttpServletRequest; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -40,17 +43,23 @@ 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.connection.DBPDriver; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; +import org.jkiss.dbeaver.model.rm.RMProjectType; +import org.jkiss.dbeaver.model.secret.DBSSecretController; import org.jkiss.dbeaver.model.security.*; import org.jkiss.dbeaver.model.security.user.SMTeam; import org.jkiss.dbeaver.model.security.user.SMUser; +import org.jkiss.dbeaver.utils.GeneralUtils; import org.jkiss.utils.CommonUtils; import java.text.MessageFormat; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Web service implementation @@ -70,6 +79,9 @@ public class WebServiceAdmin implements DBWServiceAdmin { public AdminUserInfo getUserById(@NotNull WebSession webSession, @NotNull String userId) throws DBWebException { try { SMUser smUser = webSession.getAdminSecurityController().getUserById(userId); + if (smUser == null) { + throw new DBException("User '" + userId + "' not found"); + } return new AdminUserInfo(webSession, new WebUser(smUser)); } catch (Exception e) { throw new DBWebException("Error getting user - " + userId, e); @@ -151,12 +163,19 @@ public AdminUserInfo createUser( if (userName.isEmpty()) { throw new DBWebException("Empty user name"); } - webSession.addInfoMessage("Create new user - " + userName); +// String userId = userName.toLowerCase(); + String userId = userName; + try { + GeneralUtils.validateResourceNameUnconditionally(userId); + } catch (DBException e) { + throw new DBWebException(e.getMessage(), e); + } + webSession.addInfoMessage("Create new user - " + userId); try { var securityController = webSession.getAdminSecurityController(); - securityController.createUser(userName, Map.of(), enabled, authRole); - var smUser = securityController.getUserById(userName); + securityController.createUser(userId, Map.of(), enabled, authRole); + var smUser = securityController.getUserById(userId); return new AdminUserInfo(webSession, new WebUser(smUser)); } catch (Exception e) { throw new DBWebException("Error creating new user", e); @@ -168,6 +187,11 @@ public List listAuthRoles() { return CBApplication.getInstance().getAvailableAuthRoles(); } + @Override + public List listTeamRoles() { + return CBApplication.getInstance().getAvailableTeamRoles(); + } + @Override public boolean deleteUser(@NotNull WebSession webSession, String userName) throws DBWebException { if (CommonUtils.equalObjects(userName, webSession.getUser().getUserId())) { @@ -175,23 +199,52 @@ public boolean deleteUser(@NotNull WebSession webSession, String userName) throw } webSession.addInfoMessage("Delete user - " + userName); try { + var secretController = DBSSecretController.getSessionSecretControllerOrNull(webSession); + if (secretController != null) { + secretController.deleteSubjectSecrets(userName); + } webSession.getAdminSecurityController().deleteUser(userName); - return true; } catch (Exception e) { throw new DBWebException("Error deleting user", e); } + try { + webSession.getRmController().deleteProject(RMProjectType.USER + "_" + userName); + } catch (DBException e) { + log.error("Error deleting user project", e); + webSession.addSessionError(e); + } + return true; } @NotNull @Override - public AdminTeamInfo createTeam(@NotNull WebSession webSession, String teamId, String teamName, String description) throws DBWebException { + public AdminTeamInfo createTeam( + @NotNull WebSession webSession, + @NotNull String teamId, + @Nullable String teamName, + @Nullable String description + ) throws DBWebException { if (teamId.isEmpty()) { throw new DBWebException("Empty team ID"); } + WebUser user = webSession.getUser(); + if (user == null) { + throw new DBWebException("Admin user is not found"); + } + try { + GeneralUtils.validateResourceNameUnconditionally(teamId); + } catch (DBException e) { + throw new DBWebException(e.getMessage(), e); + } + webSession.addInfoMessage("Create new team - " + teamId); try { - webSession.getAdminSecurityController().createTeam(teamId, teamName, description, webSession.getUser().getUserId()); - SMTeam newTeam = webSession.getAdminSecurityController().findTeam(teamId); + SMTeam newTeam = webSession.getAdminSecurityController().createTeam( + teamId, + teamName, + description, + user.getUserId() + ); return new AdminTeamInfo(webSession, newTeam); } catch (Exception e) { throw new DBWebException("Error creating new team", e); @@ -226,6 +279,10 @@ public boolean deleteTeam(@NotNull WebSession webSession, String teamId, boolean if (Arrays.stream(userTeams).anyMatch(team -> team.getTeamId().equals(teamId))) { throw new DBWebException("You can not delete your own team"); } + var secretController = DBSSecretController.getSessionSecretControllerOrNull(webSession); + if (secretController != null) { + secretController.deleteSubjectSecrets(teamId); + } adminSecurityController.deleteTeam(teamId, force); return true; } catch (Exception e) { @@ -239,18 +296,15 @@ public boolean grantUserTeam(@NotNull WebSession webSession, String user, String if (grantor == null) { throw new DBWebException("Cannot grant team in anonymous mode"); } - if (CommonUtils.equalObjects(user, webSession.getUser().getUserId())) { + if (!ServletAppUtils.getServletApplication().isDistributed() + && CommonUtils.equalObjects(user, webSession.getUser().getUserId()) + ) { throw new DBWebException("You cannot edit your own permissions"); } try { var adminSecurityController = webSession.getAdminSecurityController(); - SMTeam[] userTeams = adminSecurityController.getUserTeams(user); - List teamIds = Arrays.stream(userTeams).map(SMTeam::getTeamId).collect(Collectors.toList()); - if (teamIds.contains(team)) { - return true; - } - teamIds.add(team); - adminSecurityController.setUserTeams(user, teamIds.toArray(new String[0]), grantor.getUserId()); + adminSecurityController.addUserTeams(user, new String[]{team}, grantor.getUserId()); + return true; } catch (Exception e) { throw new DBWebException("Error granting team", e); @@ -263,7 +317,9 @@ public boolean revokeUserTeam(@NotNull WebSession webSession, String user, Strin if (grantor == null) { throw new DBWebException("Cannot revoke team in anonymous mode"); } - if (CommonUtils.equalObjects(user, webSession.getUser().getUserId())) { + if (!ServletAppUtils.getServletApplication().isDistributed() && + CommonUtils.equalObjects(user, webSession.getUser().getUserId()) + ) { throw new DBWebException("You cannot edit your own permissions"); } try { @@ -271,8 +327,7 @@ public boolean revokeUserTeam(@NotNull WebSession webSession, String user, Strin SMTeam[] userTeams = adminSecurityController.getUserTeams(user); List teamIds = Arrays.stream(userTeams).map(SMTeam::getTeamId).collect(Collectors.toList()); if (teamIds.contains(team)) { - teamIds.remove(team); - adminSecurityController.setUserTeams(user, teamIds.toArray(new String[0]), grantor.getUserId()); + adminSecurityController.deleteUserTeams(user, new String[]{team}); } else { throw new DBWebException("User '" + user + "' doesn't have team '" + team + "'"); } @@ -352,7 +407,7 @@ public Boolean enableUser(@NotNull WebSession webSession, @NotNull String userID } webSession.addInfoMessage("Enable user - " + userID); try { - webSession.getAdminSecurityController().enableUser(userID, enabled); + webSession.getAdminSecurityController().enableUser(userID, enabled, grantor.getUserId(), "Disabled manually"); return true; } catch (Exception e) { throw new DBWebException("Error activating user", e); @@ -362,6 +417,7 @@ public Boolean enableUser(@NotNull WebSession webSession, @NotNull String userID @Override public Boolean setUserAuthRole(WebSession webSession, String userId, String authRole) throws DBWebException { try { + log.info(String.format("User set auth role: [grantorUserId=%s]", webSession.getUserId())); webSession.getAdminSecurityController().setUserAuthRole(userId, authRole); return true; } catch (Exception e) { @@ -369,6 +425,21 @@ public Boolean setUserAuthRole(WebSession webSession, String userId, String auth } } + @Override + public Boolean setUserTeamRole( + @NotNull WebSession webSession, + @NotNull String userId, + @NotNull String teamId, + @Nullable String teamRole + ) throws DBWebException { + try { + webSession.getAdminSecurityController().setUserTeamRole(userId, teamId, teamRole); + return true; + } catch (Exception e) { + throw new DBWebException("Error updating user auth role", e); + } + } + //////////////////////////////////////////////////////////////////// // Connection management @@ -396,16 +467,35 @@ public List listAuthProviderConfigurationParameters(@NotNull We if (authProvider == null) { throw new DBWebException("Invalid provider ID " + providerId); } - return authProvider.getConfigurationParameters().stream().filter(p -> { - if (p.hasFeature("distributed")) { - return CBApplication.getInstance().isDistributed(); - } - return true; - }).map(p -> new WebPropertyInfo(webSession, p)).collect(Collectors.toList()); + var application = CBApplication.getInstance(); + + + Stream commonPropertiesStream = WebAuthProviderRegistry.getInstance() + .getCommonProperties() + .stream() + .filter(commonProperties -> commonProperties.isApplicableFor(authProvider)) + .flatMap(commonProperties -> commonProperties.getConfigurationParameters().stream()); + + return Stream.concat(authProvider.getConfigurationParameters().stream(), commonPropertiesStream) + .filter(p -> { + boolean allFeaturesEnabled = true; + for (String feature : p.getRequiredFeatures()) { + allFeaturesEnabled = application.getAppConfiguration().isFeatureEnabled(feature); + if (!allFeaturesEnabled) { + break; + } + } + return allFeaturesEnabled; + }).map(p -> new WebPropertyInfo(webSession, p)).collect(Collectors.toList()); } @Override - public List listAuthProviderConfigurations(@NotNull WebSession webSession, @Nullable String providerId) throws DBWebException { + public List listAuthProviderConfigurations( + @NotNull HttpServletRequest request, + @NotNull WebSession webSession, + @Nullable String providerId + ) throws DBWebException { + String origin = ServletAppUtils.getOriginFromRequest(request); List result = new ArrayList<>(); for (SMAuthProviderCustomConfiguration cfg : CBApplication.getInstance().getAppConfiguration().getAuthCustomConfigurations()) { if (providerId != null && !providerId.equals(cfg.getProvider())) { @@ -413,7 +503,7 @@ public List listAuthProviderConfigurations(@NotNul } WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(cfg.getProvider()); if (authProvider != null) { - result.add(new WebAuthProviderConfiguration(authProvider, cfg)); + result.add(new WebAuthProviderConfiguration(authProvider, cfg, origin)); } } return result; @@ -421,6 +511,7 @@ public List listAuthProviderConfigurations(@NotNul @Override public WebAuthProviderConfiguration saveAuthProviderConfiguration( + @NotNull HttpServletRequest request, @NotNull WebSession webSession, @NotNull String providerId, @NotNull String id, @@ -448,7 +539,13 @@ public WebAuthProviderConfiguration saveAuthProviderConfiguration( } catch (DBException e) { throw new DBWebException("Error saving server configuration", e); } - return new WebAuthProviderConfiguration(authProvider, providerConfig); + log.info(String.format( + "Auth provider configuration created: [id=%s, provider=%s, userId=%s]", + providerConfig.getId(), + providerConfig.getProvider(), + webSession.getUserId() + )); + return new WebAuthProviderConfiguration(authProvider, providerConfig, ServletAppUtils.getOriginFromRequest(request)); } @Override @@ -461,6 +558,7 @@ public boolean deleteAuthProviderConfiguration(@NotNull WebSession webSession, @ } catch (DBException e) { throw new DBWebException("Error saving server configuration", e); } + log.info(String.format("Auth provider configuration deleted: [id=%s, userId=%s]", id, webSession.getUserId())); return true; } return false; @@ -474,11 +572,12 @@ public boolean deleteAuthProviderConfiguration(@NotNull WebSession webSession, @ public boolean configureServer(WebSession webSession, Map params) throws DBWebException { try { CBAppConfig appConfig = new CBAppConfig(CBApplication.getInstance().getAppConfiguration()); + CBServerConfig serverConfig = new CBServerConfig(); + serverConfig.setServerName(CBApplication.getInstance().getServerName()); + serverConfig.setServerURL(CBApplication.getInstance().getServerURL()); + serverConfig.setMaxSessionIdleTime(CBApplication.getInstance().getMaxSessionIdleTime()); String adminName = null; String adminPassword = null; - String serverName = CBApplication.getInstance().getServerName(); - String serverURL = CBApplication.getInstance().getServerURL(); - long sessionExpireTime = CBApplication.getInstance().getMaxSessionIdleTime(); if (!params.isEmpty()) { // FE can send an empty configuration var config = new AdminServerConfig(params); @@ -486,10 +585,11 @@ public boolean configureServer(WebSession webSession, Map params appConfig.setSupportsCustomConnections(config.isCustomConnectionsEnabled()); appConfig.setPublicCredentialsSaveEnabled(config.isPublicCredentialsSaveEnabled()); appConfig.setAdminCredentialsSaveEnabled(config.isAdminCredentialsSaveEnabled()); - appConfig.setEnabledFeatures(config.getEnabledFeatures().toArray(new String[0])); - appConfig.setEnabledDrivers(config.getEnabledDrivers()); - appConfig.setDisabledDrivers(config.getDisabledDrivers()); + updateDisabledFeaturesConfig(appConfig, config.getEnabledFeatures()); + // custom logic for enabling embedded drivers + updateDisabledDriversConfig(appConfig, config.getDisabledDrivers()); appConfig.setResourceManagerEnabled(config.isResourceManagerEnabled()); + appConfig.setSecretManagerEnabled(config.isSecretManagerEnabled()); if (CommonUtils.isEmpty(config.getEnabledAuthProviders())) { // All of them @@ -503,9 +603,18 @@ public boolean configureServer(WebSession webSession, Map params adminName = config.getAdminName(); adminPassword = config.getAdminPassword(); - serverName = config.getServerName(); - serverURL = config.getServerURL(); - sessionExpireTime = config.getSessionExpireTime(); + serverConfig.setServerName(config.getServerName()); + serverConfig.setServerURL(config.getServerURL()); + serverConfig.setMaxSessionIdleTime(config.getSessionExpireTime()); + if (config.getForceHttps() != null) { + serverConfig.setForceHttps(config.getForceHttps()); + } + if (config.getSupportedHosts() != null) { + serverConfig.setSupportedHosts(config.getSupportedHosts()); + } + if (config.getBindSessionToIp() != null) { + serverConfig.setBindSessionToIp(config.getBindSessionToIp()); + } } if (CommonUtils.isEmpty(adminName)) { @@ -514,6 +623,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,11 +634,16 @@ 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)) { try { - wsc.configureServer(CBApplication.getInstance(), webSession, appConfig); + wsc.configureServer(CBApplication.getInstance(), webSession, serverConfig, appConfig); } catch (Exception e) { log.warn("Error configuring server by web service " + wsc.getClass().getName(), e); } @@ -537,12 +652,10 @@ public boolean configureServer(WebSession webSession, Map params boolean configurationMode = CBApplication.getInstance().isConfigurationMode(); CBApplication.getInstance().finishConfiguration( - serverName, - serverURL, adminName, adminPassword, - authInfoList, - sessionExpireTime, + authInfos, + serverConfig, appConfig, webSession ); @@ -555,13 +668,52 @@ public boolean configureServer(WebSession webSession, Map params // Just reload session state webSession.refreshUserData(); } - CBPlatform.getInstance().refreshApplicableDrivers(); + WebAppUtils.getWebApplication().getDriverRegistry().refreshApplicableDrivers(); } catch (Throwable e) { throw new DBWebException("Error configuring server", e); } return true; } + private void updateDisabledFeaturesConfig(CBAppConfig appConfig, List enabledFeatures) { + Set enabledIds = new LinkedHashSet<>(enabledFeatures); + appConfig.setEnabledFeatures(enabledFeatures.toArray(new String[0])); + String[] disabledFeatures = WebFeatureRegistry.getInstance().getWebFeatures().stream().map(DBWFeatureSet::getId) + .filter(id -> !enabledIds.contains(id)) + .toArray(String[]::new); + appConfig.setDisabledFeatures(disabledFeatures); + } + + // we disable embedded drivers by default and enable it in enabled drivers list + // that's why we need so complicated logic for disabling drivers + private void updateDisabledDriversConfig(CBAppConfig appConfig, String[] disabledDriversConfig) { + Set disabledIds = new LinkedHashSet<>(Arrays.asList(disabledDriversConfig)); + Set enabledIds = new LinkedHashSet<>(Arrays.asList(appConfig.getEnabledDrivers())); + + // remove all disabled embedded drivers from enabled drivers list + enabledIds.removeAll(disabledIds); + + // enable embedded driver if it is not in disabled drivers list + for (String driverId : appConfig.getDisabledDrivers()) { + if (disabledIds.contains(driverId)) { + // driver is also disabled + continue; + } + // driver is removed from disabled list + // we need to enable if it is embedded + try { + DBPDriver driver = WebServiceUtils.getDriverById(driverId); + if (driver.isEmbedded()) { + enabledIds.add(driverId); + } + } catch (DBWebException e) { + log.error("Failed to find driver by id", e); + } + } + appConfig.setDisabledDrivers(disabledDriversConfig); + appConfig.setEnabledDrivers(enabledIds.toArray(String[]::new)); + } + @Override public boolean setDefaultNavigatorSettings(WebSession webSession, DBNBrowseSettings settings) throws DBWebException { CBApplication.getInstance().getAppConfiguration().setDefaultNavigatorSettings(settings); @@ -576,6 +728,17 @@ public boolean setDefaultNavigatorSettings(WebSession webSession, DBNBrowseSetti return true; } + + @Override + public boolean updateProductConfiguration(WebSession webSession, Map productConfiguration) throws DBWebException { + try { + CBApplication.getInstance().saveProductConfiguration(webSession, productConfiguration); + return true; + } catch (DBException e) { + throw new DBWebException("Error updating product configuration", e); + } + } + //////////////////////////////////////////////////////////////////// // Access management @@ -610,14 +773,7 @@ public boolean setConnectionSubjectAccess( @NotNull String connectionId, @NotNull List subjects ) throws DBWebException { - DBPProject globalProject = webSession.getProjectById(projectId); - if (!WebServiceUtils.isGlobalProject(globalProject)) { - throw new DBWebException("Project '" + projectId + "'is not global"); - } - DBPDataSourceContainer dataSource = getDataSourceRegistry(webSession, projectId).getDataSource(connectionId); - if (dataSource == null) { - throw new DBWebException("Connection '" + connectionId + "' not found"); - } + validateThatConnectionGlobal(webSession, projectId, List.of(connectionId)); WebUser grantor = webSession.getUser(); if (grantor == null) { throw new DBWebException("Cannot grant connection access in anonymous mode"); @@ -635,6 +791,72 @@ public boolean setConnectionSubjectAccess( return true; } + void validateThatConnectionGlobal(WebSession webSession, String projectId, Collection connectionIds) throws DBWebException { + DBPProject globalProject = webSession.getProjectById(projectId); + if (!WebServiceUtils.isGlobalProject(globalProject)) { + throw new DBWebException("Project '" + projectId + "'is not global"); + } + for (String connectionId : connectionIds) { + DBPDataSourceContainer dataSource = getDataSourceRegistry(webSession, projectId).getDataSource(connectionId); + if (dataSource == null) { + throw new DBWebException("Connection '" + connectionId + "' not found"); + } + } + } + + @Override + public boolean addConnectionsAccess( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull List connectionIds, + @NotNull List subjects + ) throws DBWebException { + validateThatConnectionGlobal(webSession, projectId, connectionIds); + WebUser grantor = webSession.getUser(); + if (grantor == null) { + throw new DBWebException("Cannot grant connection access in anonymous mode"); + } + try { + var adminSM = webSession.getAdminSecurityController(); + adminSM.addObjectPermissions( + new HashSet<>(connectionIds), + SMObjectType.datasource, + new HashSet<>(subjects), + Set.of(SMConstants.DATA_SOURCE_ACCESS_PERMISSION), + grantor.getUserId() + ); + } catch (DBException e) { + throw new DBWebException("Error adding connection subject access", e); + } + return true; + } + + @Override + public boolean deleteConnectionsAccess( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull List connectionIds, + @NotNull List subjects + ) throws DBWebException { + validateThatConnectionGlobal(webSession, projectId, connectionIds); + WebUser grantor = webSession.getUser(); + if (grantor == null) { + throw new DBWebException("Cannot grant connection access in anonymous mode"); + } + try { + var adminSM = webSession.getAdminSecurityController(); + adminSM.deleteObjectPermissions( + new HashSet<>(connectionIds), + SMObjectType.datasource, + new HashSet<>(subjects), + Set.of(SMConstants.DATA_SOURCE_ACCESS_PERMISSION) + ); + } catch (DBException e) { + throw new DBWebException("Error adding connection subject access", e); + } + return true; + } + @Override public SMDataSourceGrant[] getSubjectConnectionAccess(@NotNull WebSession webSession, @NotNull String subjectId) throws DBWebException { try { diff --git a/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF index 69fa39153b..7868fc5ccc 100644 --- a/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF @@ -3,11 +3,10 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Authentication Bundle-SymbolicName: io.cloudbeaver.service.auth;singleton:=true -Bundle-Version: 1.0.82.qualifier -Bundle-Release-Date: 20230209 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 1.0.129.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy -Bundle-ClassPath: . -Require-Bundle: io.cloudbeaver.server +Require-Bundle: io.cloudbeaver.server.ce Automatic-Module-Name: io.cloudbeaver.service.auth Export-Package: io.cloudbeaver.service.auth diff --git a/server/bundles/io.cloudbeaver.service.auth/plugin.xml b/server/bundles/io.cloudbeaver.service.auth/plugin.xml index 01901218ac..d34aea2e28 100644 --- a/server/bundles/io.cloudbeaver.service.auth/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.auth/plugin.xml @@ -7,6 +7,9 @@ + + @@ -16,4 +19,10 @@ + + + + + + diff --git a/server/bundles/io.cloudbeaver.service.auth/pom.xml b/server/bundles/io.cloudbeaver.service.auth/pom.xml index 347228f83d..e7019364eb 100644 --- a/server/bundles/io.cloudbeaver.service.auth/pom.xml +++ b/server/bundles/io.cloudbeaver.service.auth/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.auth - 1.0.82-SNAPSHOT + 1.0.129-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls index dd4f55c03e..24960ec2de 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -30,16 +30,22 @@ type AuthProviderConfiguration { id: ID! displayName: String! disabled: Boolean! + authRoleProvided: Boolean iconURL: String description: String - # URL to external authentication service. - # If specified then it is external auhentication provider (SSO). - # Otherwise authLogin function must be called. + """ + URL to external authentication service. + If specified then it is external authentication provider (SSO). + Otherwise authLogin function must be called. + """ signInLink: String signOutLink: String + redirectLink: String metadataLink: String + acsLink: String + entityIdLink: String @since(version: "24.2.1") } type AuthProviderCredentialsProfile { @@ -58,100 +64,152 @@ type AuthProviderInfo { defaultProvider: Boolean! trusted: Boolean! private: Boolean! + authHidden: Boolean! @since(version: "24.2.4") supportProvisioning: Boolean! - # Configurable providers must be configured first. See configurations field. + "Configurable providers must be configured first. See configurations field." configurable: Boolean! - # Federated providers means authorization must occur asynchronously through redirects. + "Federated providers means authorization must occur asynchronously through redirects." federated: Boolean! - # Provider configurations (applicable only if configurable=true) + "Provider configurations (applicable only if configurable=true)" configurations: [AuthProviderConfiguration!] + templateConfiguration: AuthProviderConfiguration! @since(version: "24.1.2") + credentialProfiles: [AuthProviderCredentialsProfile!]! requiredFeatures: [String!]! + + required: Boolean! } + type AuthInfo { - redirectLink: String + redirectLink: String @deprecated - authId: String + authId: String @deprecated - authStatus: AuthStatus! + authStatus: AuthStatus! @deprecated userTokens: [UserAuthToken!] } +type FederatedAuthInfo @since(version: "25.0.3") { + redirectLink: String! + taskInfo: AsyncTaskInfo! +} + +type FederatedAuthResult @since(version: "25.0.3") { + userTokens: [UserAuthToken!]! @since(version: "25.0.3") +} + +type LogoutInfo @since(version: "23.3.3") { + redirectLinks: [String!]! +} + type UserAuthToken { - # Auth provider used for authorization + "Auth provider used for authorization" authProvider: ID! - # Auth provider configuration ID + "Auth provider configuration ID" authConfiguration: ID - # Authorization time + "Authorization time" loginTime: DateTime! - # User identity (aka user name) specific to auth provider + "User identity (aka user name) specific to auth provider" userId: String! - # User display name specific to auth provider + "User display name specific to auth provider" displayName: String! - # Optional login message + "Optional login message" message: String - # Auth origin + "Auth origin" origin: ObjectOrigin! } type UserInfo { - # User unique identifier + "User unique identifier" userId: ID! - # Human readable display name. It is taken from the first auth provider which was used for user login. + "Human readable display name. It is taken from the first auth provider which was used for user login." displayName: String - # User auth role ID. Optional. + "User auth role ID. Optional." authRole: ID - # All authentication tokens used during current session + "All authentication tokens used during current session" authTokens: [UserAuthToken!]! linkedAuthProviders: [String!]! - # User profile properties map + "User profile properties map" metaParameters: Object! - # User configuration parameters + "User configuration parameters" configurationParameters: Object! + "User teams" + teams: [UserTeamInfo!]! + + "Indicates whether the user is anonymous (not authenticated)." + isAnonymous: Boolean! @since(version: "24.2.3") +} +type UserTeamInfo { + teamId: String! + teamName: String! + teamRole: String } extend type Query { - # Authorize user using specified auth provider. If linkUser=true then associates new - authLogin(provider: ID!, configuration: ID, credentials: Object, linkUser: Boolean): AuthInfo! + """ + Authorizes the user using specified auth provider. + Associates new credentials with the active user when linkUser=true. + Kills another user sessions if forceSessionsLogout=true. + """ + authLogin(provider: ID!, configuration: ID, credentials: Object, linkUser: Boolean, forceSessionsLogout: Boolean): AuthInfo! - authUpdateStatus(authId: ID!, linkUser: Boolean): AuthInfo! + "Returns result of federated authentication task" + federatedAuthTaskResult(taskId: String!): FederatedAuthResult! @since(version: "25.0.3") - # Logouts user. If provider not specified then all authorizations are revoked from session. - authLogout(provider: ID, configuration: ID): Boolean + authUpdateStatus(authId: ID!, linkUser: Boolean): AuthInfo! @deprecated - # Active user information. null is no user was authorized within session + "Same as authLogoutExtended but without additional information" + authLogout(provider: ID, configuration: ID): Boolean @deprecated(reason: "use authLogoutExtended instead") + + "Logouts user. If provider not specified then all authorizations are revoked from session. Contains additional information" + authLogoutExtended(provider: ID, configuration: ID): LogoutInfo! @since(version: "23.3.3") + + "Active user information. null is no user was authorized within session" activeUser: UserInfo + "Returns list of all available auth providers" authProviders: [AuthProviderInfo!]! + "Changes the local password of the current user" authChangeLocalPassword(oldPassword: String!, newPassword: String!): Boolean! + "Returns properties that can be shown in user profile" listUserProfileProperties: [ObjectPropertyInfo!]! } + extend type Mutation { - # Set user config parameter. If parameter value is null then removes the parameter + "Set user config parameter. If parameter value is null then removes the parameter" setUserConfigurationParameter(name: String!, value: Object): Boolean! - + "Updates user preferences" + setUserPreferences(preferences: Object!): UserInfo! @since(version: "24.0.1") + + "Creates async task for federated login. Returns redirect link to the task info page." + federatedLogin( + provider: ID!, + configuration: ID, + linkUser: Boolean, + forceSessionsLogout: Boolean + ): FederatedAuthInfo! @since(version: "25.0.3") } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java index 6fc4a9e512..400a344e39 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -20,8 +20,9 @@ import io.cloudbeaver.WebAction; import io.cloudbeaver.model.WebPropertyInfo; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.model.user.WebAuthProviderInfo; import io.cloudbeaver.service.DBWService; +import io.cloudbeaver.service.auth.model.user.WebAuthProviderInfo; +import jakarta.servlet.http.HttpServletRequest; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -38,20 +39,42 @@ WebAuthStatus authLogin( @NotNull String providerId, @Nullable String providerConfigurationId, @Nullable Map credentials, - boolean linkWithActiveUser) throws DBWebException; + boolean linkWithActiveUser, + boolean forceSessionsLogout + ) throws DBWebException; + @WebAction(authRequired = false) + WebAsyncAuthStatus federatedLogin( + @NotNull HttpServletRequest httpRequest, + @NotNull WebSession webSession, + @NotNull String providerId, + @Nullable String providerConfigurationId, + boolean linkWithActiveUser, + boolean forceSessionsLogout + ) throws DBWebException; + + @WebAction(authRequired = false) + WebAsyncAuthTaskResult federatedAuthTaskResult( + @NotNull WebSession webSession, + @NotNull String taskId + ) throws DBWebException; @WebAction(authRequired = false) WebAuthStatus authUpdateStatus(@NotNull WebSession webSession, @NotNull String authId, boolean linkWithActiveUser) throws DBWebException; @WebAction(authRequired = false) - void authLogout(@NotNull WebSession webSession, @Nullable String providerId, @Nullable String configurationId) throws DBWebException; + WebLogoutInfo authLogout( + @NotNull HttpServletRequest httpRequest, + @NotNull WebSession webSession, + @Nullable String providerId, + @Nullable String configurationId + ) throws DBWebException; @WebAction(authRequired = false) WebUserInfo activeUser(@NotNull WebSession webSession) throws DBWebException; @WebAction(authRequired = false) - WebAuthProviderInfo[] getAuthProviders(); + WebAuthProviderInfo[] getAuthProviders(@NotNull HttpServletRequest request) throws DBWebException; @WebAction() boolean changeLocalPassword(@NotNull WebSession webSession, @NotNull String oldPassword, @NotNull String newPassword) throws DBWebException; @@ -65,4 +88,10 @@ boolean setUserConfigurationParameter( @NotNull String name, @Nullable String value) throws DBWebException; + @WebAction() + WebUserInfo setUserConfigurationParameters( + @NotNull WebSession webSession, + @NotNull Map parameters + ) throws DBWebException; + } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPConstants.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPConstants.java new file mode 100644 index 0000000000..0341ab59a7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPConstants.java @@ -0,0 +1,28 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.auth; + +interface RPConstants { + String PARAM_LOGOUT_URL = "logout-url"; + String PARAM_USER = "user-header"; + String PARAM_TEAM = "team-header"; + String PARAM_FIRST_NAME = "first-name-header"; + String PARAM_LAST_NAME = "last-name-header"; + String PARAM_FULL_NAME = "full-name-header"; + String PARAM_ROLE_NAME = "role-header"; + String PARAM_TEAM_DELIMITER = "team-delimiter"; +} diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java index 11e0ebb054..7544f4ad8a 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -19,18 +19,22 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.auth.SMAuthProviderExternal; import io.cloudbeaver.auth.provider.rp.RPAuthProvider; -import io.cloudbeaver.model.app.WebAuthConfiguration; +import io.cloudbeaver.model.app.ServletAuthConfiguration; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebSessionAuthProcessor; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.service.DBWSessionHandler; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.utils.ServletAppUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; import org.jkiss.dbeaver.model.security.SMConstants; import org.jkiss.dbeaver.model.security.SMController; import org.jkiss.dbeaver.model.security.SMStandardMeta; @@ -38,22 +42,22 @@ import org.jkiss.utils.CommonUtils; import java.io.IOException; -import java.util.Collections; +import java.text.MessageFormat; import java.util.HashMap; import java.util.List; import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; public class RPSessionHandler implements DBWSessionHandler { private static final Log log = Log.getLog(RPSessionHandler.class); + public static final String DEFAULT_TEAM_DELIMITER = "\\|"; @Override public boolean handleSessionOpen(WebSession webSession, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException { boolean configMode = CBApplication.getInstance().isConfigurationMode(); //checks if the app is not in configuration mode and reverse proxy auth is enabled in the config file - WebAuthConfiguration appConfiguration = (WebAuthConfiguration) WebAppUtils.getWebApplication().getAppConfiguration(); + ServletAuthConfiguration appConfiguration = (ServletAuthConfiguration) ServletAppUtils.getServletApplication() + .getAppConfiguration(); boolean isReverseProxyAuthEnabled = appConfiguration.isAuthProviderEnabled(RPAuthProvider.AUTH_PROVIDER); if (!configMode && isReverseProxyAuthEnabled) { reverseProxyAuthentication(request, webSession); @@ -61,18 +65,47 @@ public boolean handleSessionOpen(WebSession webSession, HttpServletRequest reque return false; } - public void reverseProxyAuthentication(@NotNull HttpServletRequest request, @NotNull WebSession webSession) throws DBWebException { + public void reverseProxyAuthentication(@NotNull HttpServletRequest request, @NotNull WebSession webSession) throws DBException { SMController securityController = webSession.getSecurityController(); WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(RPAuthProvider.AUTH_PROVIDER); if (authProvider == null) { throw new DBWebException("Auth provider " + RPAuthProvider.AUTH_PROVIDER + " not found"); } SMAuthProviderExternal authProviderExternal = (SMAuthProviderExternal) authProvider.getInstance(); - String userName = request.getHeader(RPAuthProvider.X_USER); - String teams = request.getHeader(RPAuthProvider.X_ROLE); - String firstName = request.getHeader(RPAuthProvider.X_FIRST_NAME); - String lastName = request.getHeader(RPAuthProvider.X_LAST_NAME); - List userTeams = teams == null ? Collections.emptyList() : List.of(teams.split("\\|")); + SMAuthProviderCustomConfiguration configuration = ServletAppUtils.getAuthApplication() + .getAuthConfiguration() + .getAuthCustomConfigurations() + .stream() + .filter(p -> p.getProvider().equals(authProvider.toString())) + .findFirst() + .orElse(null); + Map paramConfigMap = new HashMap<>(); + if (configuration != null) { + authProvider.getConfigurationParameters().forEach(p -> + paramConfigMap.put(p.getId(), configuration.getParameters().get(p.getId()) + )); + } + String userName = request.getHeader( + resolveParam(paramConfigMap.get(RPConstants.PARAM_USER), RPAuthProvider.X_USER) + ); + String teams = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_TEAM), RPAuthProvider.X_TEAM)); + // backward compatibility + String deprecatedTeams = request.getHeader(RPAuthProvider.X_ROLE); + if (teams == null && deprecatedTeams != null) { + teams = deprecatedTeams; + } + String role = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_ROLE_NAME), RPAuthProvider.X_ROLE_TE)); + String firstName = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_FIRST_NAME), RPAuthProvider.X_FIRST_NAME)); + String lastName = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_LAST_NAME), RPAuthProvider.X_LAST_NAME)); + String fullName = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_FULL_NAME), RPAuthProvider.X_FULL_NAME)); + String logoutUrl = null; + String teamDelimiter = DEFAULT_TEAM_DELIMITER; + if (configuration != null) { + logoutUrl = configuration.getParameter(RPConstants.PARAM_LOGOUT_URL); + teamDelimiter = resolveParam(JSONUtils.getString(configuration.getParameters(), + RPConstants.PARAM_TEAM_DELIMITER), DEFAULT_TEAM_DELIMITER); + } + List userTeams = teams == null ? null : (teams.isEmpty() ? List.of() : List.of(teams.split(teamDelimiter))); if (userName != null) { try { Map credentials = new HashMap<>(); @@ -83,18 +116,29 @@ public void reverseProxyAuthentication(@NotNull HttpServletRequest request, @Not if (!CommonUtils.isEmpty(lastName)) { credentials.put(SMStandardMeta.META_LAST_NAME, lastName); } + if (!CommonUtils.isEmpty(fullName)) { + credentials.put("fullName", fullName); + } + if (CommonUtils.isNotEmpty(logoutUrl)) { + credentials.put("logoutUrl", logoutUrl); + } Map sessionParameters = webSession.getSessionParameters(); sessionParameters.put(SMConstants.SESSION_PARAM_TRUSTED_USER_TEAMS, userTeams); + sessionParameters.put(SMConstants.SESSION_PARAM_TRUSTED_USER_ROLE, role); Map userCredentials = authProviderExternal.authExternalUser( webSession.getProgressMonitor(), null, credentials); String currentSmSessionId = webSession.getUser() == null ? null : webSession.getUserContext().getSmSessionId(); try { + log.debug(MessageFormat.format( + "Attempting to authenticate user ''{0}'' with teams {1} through reverse proxy", userName, userTeams)); SMAuthInfo smAuthInfo = securityController.authenticate( webSession.getSessionId(), currentSmSessionId, sessionParameters, - WebSession.CB_SESSION_TYPE, authProvider.getId(), null, userCredentials); + WebSession.CB_SESSION_TYPE, authProvider.getId(), configuration.getId(), userCredentials, false); new WebSessionAuthProcessor(webSession, smAuthInfo, false).authenticateSession(); + log.debug(MessageFormat.format( + "Successful reverse proxy authentication: user ''{0}'' with teams {1}", userName, userTeams)); } catch (SMException e) { log.debug("Error during user authentication", e); throw e; @@ -109,4 +153,11 @@ public void reverseProxyAuthentication(@NotNull HttpServletRequest request, @Not public boolean handleSessionClose(WebSession webSession) throws DBException, IOException { return false; } + + private String resolveParam(Object value, String defaultValue) { + if (value != null && !value.toString().isEmpty()) { + return value.toString(); + } + return defaultValue; + } } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/ReverseProxyConfigurator.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/ReverseProxyConfigurator.java new file mode 100644 index 0000000000..a73ebcb294 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/ReverseProxyConfigurator.java @@ -0,0 +1,113 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.auth; + +import io.cloudbeaver.auth.provider.rp.RPAuthProvider; +import io.cloudbeaver.model.app.ServletAppConfiguration; +import io.cloudbeaver.model.app.ServletApplication; +import io.cloudbeaver.model.app.ServletAuthApplication; +import io.cloudbeaver.model.app.ServletServerConfiguration; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.service.DBWServiceServerConfigurator; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; + +import java.util.HashMap; +import java.util.Map; + +public class ReverseProxyConfigurator implements DBWServiceServerConfigurator { + private static final Log log = Log.getLog(ReverseProxyConfigurator.class); + + @Override + public void configureServer( + @NotNull ServletApplication application, + @Nullable WebSession session, + @NotNull ServletServerConfiguration serverConfiguration, + @NotNull ServletAppConfiguration appConfig + ) throws DBException { + } + + @Override + public void migrateConfigurationIfNeeded(@NotNull ServletApplication application) throws DBException { + if (migrationNotNeeded(application)) { + return; + } + migrateConfiguration(application); + } + + @Override + public void reloadConfiguration(@NotNull ServletAppConfiguration appConfig) throws DBException { + + } + + private void migrateConfiguration( + @NotNull ServletApplication application + ) { + if (!(application instanceof ServletAuthApplication authApplication)) { + return; + } + + SMAuthProviderCustomConfiguration smReverseProxyProviderConfiguration = + authApplication.getAuthConfiguration().getAuthProviderConfiguration(RPAuthProvider.AUTH_PROVIDER); + if (smReverseProxyProviderConfiguration == null) { + smReverseProxyProviderConfiguration = new SMAuthProviderCustomConfiguration(RPAuthProvider.AUTH_PROVIDER); + smReverseProxyProviderConfiguration.setProvider(RPAuthProvider.AUTH_PROVIDER); + smReverseProxyProviderConfiguration.setDisplayName("Reverse Proxy"); + smReverseProxyProviderConfiguration.setDescription( + "This provider was created automatically" + ); + smReverseProxyProviderConfiguration .setIconURL(""); + Map parameters = new HashMap<>(); + parameters.put(RPConstants.PARAM_USER, RPAuthProvider.X_USER); + parameters.put(RPConstants.PARAM_TEAM, RPAuthProvider.X_TEAM); + parameters.put(RPConstants.PARAM_FIRST_NAME, RPAuthProvider.X_FIRST_NAME); + parameters.put(RPConstants.PARAM_LAST_NAME, RPAuthProvider.X_LAST_NAME); + parameters.put(RPConstants.PARAM_FULL_NAME, RPAuthProvider.X_FULL_NAME); + smReverseProxyProviderConfiguration.setParameters(parameters); + authApplication.getAuthConfiguration().addAuthProviderConfiguration(smReverseProxyProviderConfiguration ); + try { + authApplication.flushConfiguration(); + } catch (Exception e) { + log.error("Failed to save server configuration", e); + } + } + } + + private boolean migrationNotNeeded(@NotNull ServletApplication application) { + if (!(application instanceof ServletAuthApplication authApplication)) { + return true; + } + + if (!authApplication.getAuthConfiguration().isAuthProviderEnabled(RPAuthProvider.AUTH_PROVIDER)) { + log.debug("Reverse proxy provider disabled, migration not needed"); + return true; + } + + boolean isReverseProxyConfigured = authApplication.getAuthConfiguration() + .getAuthCustomConfigurations().stream() + .anyMatch(p -> p.getProvider().equals(RPAuthProvider.AUTH_PROVIDER)); + + if (isReverseProxyConfigured) { + log.debug("Reverse proxy provider already exist, migration not needed"); + return true; + } + return false; + } +} diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthJob.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthJob.java new file mode 100644 index 0000000000..b10bde7c96 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthJob.java @@ -0,0 +1,78 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.auth; + +import io.cloudbeaver.model.CustomCancelableJob; +import io.cloudbeaver.model.WebAsyncTaskInfo; +import io.cloudbeaver.model.session.WebAuthInfo; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.utils.WebEventUtils; +import org.eclipse.core.runtime.IStatus; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.runtime.AbstractJob; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; + +import java.util.List; + +public class WebAsyncAuthJob extends AbstractJob implements CustomCancelableJob { + @NotNull + private final String authId; + private final boolean linkWithUser; + //result from task do used, because it cannot be serialized into 'object' gql type and separate request is used + //to get auth result + @Nullable + private List authResult; + + public WebAsyncAuthJob(@NotNull String name, @NotNull String authId, boolean linkWithUser) { + super(name); + this.authId = authId; + this.linkWithUser = linkWithUser; + } + + //do nothing, this job is workaround to use exist async process + @Override + protected IStatus run(DBRProgressMonitor monitor) { + return null; + } + + @NotNull + public String getAuthId() { + return authId; + } + + public boolean isLinkWithUser() { + return linkWithUser; + } + + @Nullable + public List getAuthResult() { + return authResult; + } + + public void setAuthResult(@Nullable List authResult) { + this.authResult = authResult; + } + + @Override + public void cancelJob(@NotNull WebSession webSession, @NotNull WebAsyncTaskInfo taskInfo) { + taskInfo.setRunning(false); + taskInfo.setJobError(new DBException("Canceled by the user")); + WebEventUtils.sendAsyncTaskEvent(webSession, taskInfo); + } +} diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthStatus.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthStatus.java new file mode 100644 index 0000000000..1e1b698182 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthStatus.java @@ -0,0 +1,46 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.auth; + +import io.cloudbeaver.model.WebAsyncTaskInfo; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.meta.Property; + +public class WebAsyncAuthStatus { + @NotNull + private final String redirectLink; + + @NotNull + private final WebAsyncTaskInfo taskInfo; + + public WebAsyncAuthStatus(@NotNull String redirectLink, @NotNull WebAsyncTaskInfo taskInfo) { + this.redirectLink = redirectLink; + this.taskInfo = taskInfo; + } + + @Property + @NotNull + public String getRedirectLink() { + return redirectLink; + } + + @NotNull + @Property + public WebAsyncTaskInfo getTaskInfo() { + return taskInfo; + } +} diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthTaskResult.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthTaskResult.java new file mode 100644 index 0000000000..c5f789bc6f --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthTaskResult.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.auth; + +import io.cloudbeaver.model.session.WebAuthInfo; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.meta.Property; + +import java.util.List; + +public class WebAsyncAuthTaskResult { + @NotNull + private final List userTokens; + + public WebAsyncAuthTaskResult(@NotNull List userTokens) { + this.userTokens = userTokens; + } + + @NotNull + @Property + public List getUserTokens() { + return userTokens; + } +} diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebLogoutInfo.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebLogoutInfo.java new file mode 100644 index 0000000000..9e8aaefb3d --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebLogoutInfo.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.auth; + +import org.jkiss.code.NotNull; + +import java.util.List; + +public record WebLogoutInfo(@NotNull List redirectLinks) { +} diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java index 5ee8b712b9..abd9a74976 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -17,6 +17,7 @@ package io.cloudbeaver.service.auth; import io.cloudbeaver.DBWebException; +import io.cloudbeaver.server.graphql.GraphQLEndpoint; import io.cloudbeaver.service.DBWBindingContext; import io.cloudbeaver.service.WebServiceBindingBase; import io.cloudbeaver.service.auth.impl.WebServiceAuthImpl; @@ -41,22 +42,34 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env.getArgument("provider"), env.getArgument("configuration"), env.getArgument("credentials"), - CommonUtils.toBoolean(env.getArgument("linkUser")))) + CommonUtils.toBoolean(env.getArgument("linkUser")), + CommonUtils.toBoolean(env.getArgument("forceSessionsLogout")) + )) + .dataFetcher("federatedAuthTaskResult", env -> getService(env).federatedAuthTaskResult( + getWebSession(env, false), + env.getArgument("taskId") + )) + .dataFetcher("authLogoutExtended", env -> getService(env).authLogout( + GraphQLEndpoint.getServletRequestOrThrow(env), + getWebSession(env, false), + env.getArgument("provider"), + env.getArgument("configuration") + )) .dataFetcher("authLogout", env -> { getService(env).authLogout( + GraphQLEndpoint.getServletRequestOrThrow(env), getWebSession(env, false), env.getArgument("provider"), - env.getArgument("configuration") - ); + env.getArgument("configuration")); return true; }) .dataFetcher("authUpdateStatus", env -> getService(env).authUpdateStatus( - getWebSession(env), + getWebSession(env, false), env.getArgument("authId"), CommonUtils.toBoolean(env.getArgument("linkUser")) )) .dataFetcher("activeUser", env -> getService(env).activeUser(getWebSession(env, false))) - .dataFetcher("authProviders", env -> getService(env).getAuthProviders()) + .dataFetcher("authProviders", env -> getService(env).getAuthProviders(GraphQLEndpoint.getServletRequestOrThrow(env))) .dataFetcher("authChangeLocalPassword", env -> getService(env).changeLocalPassword( getWebSession(env), env.getArgument("oldPassword"), @@ -70,6 +83,17 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env -> getService(env).setUserConfigurationParameter(getWebSession(env), env.getArgument("name"), env.getArgument("value"))) + .dataFetcher("setUserPreferences", + env -> getService(env).setUserConfigurationParameters(getWebSession(env), + env.getArgument("preferences"))) + .dataFetcher("federatedLogin", env -> getService(env).federatedLogin( + GraphQLEndpoint.getServletRequestOrThrow(env), + getWebSession(env, false), + env.getArgument("provider"), + env.getArgument("configuration"), + CommonUtils.toBoolean(env.getArgument("linkUser")), + CommonUtils.toBoolean(env.getArgument("forceSessionsLogout")) + )) ; } } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserAuthToken.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserAuthToken.java index bc2b9c042b..9102438a23 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserAuthToken.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserAuthToken.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java index ce9e4827bf..d7cbaf650c 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -19,6 +19,7 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.user.WebUser; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.meta.Property; @@ -68,6 +69,9 @@ public List getAuthTokens() { @Property public List getLinkedAuthProviders() throws DBWebException { + if (isAnonymous()) { + return List.of(); + } if (linkedProviders == null) { try { linkedProviders = session.getSecurityController().getCurrentUserLinkedProviders(); @@ -85,11 +89,27 @@ public Map getMetaParameters() { @Property public Map getConfigurationParameters() throws DBWebException { - try { - return session.getSecurityController().getCurrentUserParameters(); - } catch (DBException e) { - throw new DBWebException("Error reading user parameters", e); + return session.getUserContext().getPreferenceStore().getCustomUserParameters(); + } + + @NotNull + @Property + public List getTeams() throws DBWebException { + if (session.getUserContext().isNonAnonymousUserAuthorizedInSM()) { + try { + return Arrays.stream(session.getSecurityController().getCurrentUserTeams()) + .map(WebUserTeamInfo::new) + .toList(); + } catch (DBException e) { + throw new DBWebException("Error reading user's teams", e); + } + } else { + return List.of(); } } + @Property + public boolean isAnonymous() { + return session.getUser() == null; + } } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserTeamInfo.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserTeamInfo.java new file mode 100644 index 0000000000..029d0dd08a --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserTeamInfo.java @@ -0,0 +1,49 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.auth; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.security.user.SMUserTeam; + +public class WebUserTeamInfo { + @NotNull + private final SMUserTeam userTeam; + + public WebUserTeamInfo(@NotNull SMUserTeam userTeam) { + this.userTeam = userTeam; + } + + @NotNull + @Property + public String getTeamId() { + return userTeam.getTeamId(); + } + + @NotNull + @Property + public String getTeamName() { + return userTeam.getTeamName(); + } + + @Nullable + @Property + public String getTeamRole() { + return userTeam.getTeamRole(); + } +} diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/handler/WSAuthSessionEventHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/handler/WSAuthSessionEventHandler.java new file mode 100644 index 0000000000..753140bf38 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/handler/WSAuthSessionEventHandler.java @@ -0,0 +1,111 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.auth.handler; + +import io.cloudbeaver.DBWConstants; +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.WebAsyncTaskInfo; +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebAuthInfo; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.model.session.WebSessionAuthProcessor; +import io.cloudbeaver.server.WebAppSessionManager; +import io.cloudbeaver.server.WebAppUtils; +import io.cloudbeaver.server.WebApplication; +import io.cloudbeaver.service.auth.WebAsyncAuthJob; +import io.cloudbeaver.utils.WebEventUtils; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.auth.SMAuthInfo; +import org.jkiss.dbeaver.model.auth.SMAuthStatus; +import org.jkiss.dbeaver.model.websocket.WSEventHandler; +import org.jkiss.dbeaver.model.websocket.event.session.WSAuthEvent; + +import java.util.List; + +public class WSAuthSessionEventHandler implements WSEventHandler { + private static final Log log = Log.getLog(WSAuthSessionEventHandler.class); + + @Override + public void handleEvent(@NotNull WSAuthEvent event) { + SMAuthInfo authInfo = event.getAuthInfo(); + WebApplication webApplication = WebAppUtils.getWebApplication(); + WebAppSessionManager sessionManager = webApplication.getSessionManager(); + if (authInfo.getAuthPermissions() == null && authInfo.getAuthStatus() == SMAuthStatus.SUCCESS) { + log.error("No auth permissions available in SUCCESS auth"); + return; + } + String sessionId = authInfo.getAppSessionId(); + BaseWebSession baseWebSession = sessionManager.getSession(sessionId); + if (!(baseWebSession instanceof WebSession webSession)) { + log.trace("No web session found in current node with id '" + sessionId + "'"); + return; + } + List allAuthJobs = webSession.findTasksByJob(WebAsyncAuthJob.class); + WebAsyncTaskInfo relatedTask = allAuthJobs.stream().filter( + task -> { + WebAsyncAuthJob job = (WebAsyncAuthJob) task.getJob(); + return job.getAuthId().equals(authInfo.getAuthAttemptId()); + }) + .findFirst().orElse(null); + if (relatedTask == null) { + String message = "No related authentication task was found in'" + sessionId + "'," + + " probably authentication was canceled"; + log.warn(message); + webSession.addWarningMessage(message); + return; + } + if (!relatedTask.isRunning()) { + String message = "Related authentication task was canceled"; + log.warn(message); + webSession.addWarningMessage(message); + return; + } + WebAsyncAuthJob relatedJob = (WebAsyncAuthJob) relatedTask.getJob(); + switch (authInfo.getAuthStatus()) { + case SUCCESS: + boolean linkCredentialsWithActiveUser = !webApplication.isConfigurationMode() + && !webSession.isAuthorizedInSecurityManager(); + try { + List newInfos = new WebSessionAuthProcessor( + webSession, + authInfo, + linkCredentialsWithActiveUser + ).authenticateSession(); + relatedJob.setAuthResult(newInfos); + } catch (DBException e) { + webSession.addSessionError(e); + relatedTask.setJobError(e); + } + break; + case ERROR: + var error = new DBWebException(authInfo.getError(), authInfo.getErrorCode()); + relatedTask.setJobError(error); + break; + default: + String message = "Invalid auth status: " + authInfo.getAuthStatus(); + log.error(message); + var exception = new DBWebException(message); + webSession.addSessionError(exception); + relatedTask.setJobError(new DBWebException(message)); + } + relatedTask.setRunning(false); + relatedTask.setStatus(DBWConstants.TASK_STATUS_FINISHED); + WebEventUtils.sendAsyncTaskEvent(webSession, relatedTask); + } +} diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index 0fb04946ba..fb39b495c4 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -18,34 +18,41 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.auth.SMSignOutLinkProvider; import io.cloudbeaver.auth.provider.local.LocalAuthProvider; +import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.WebPropertyInfo; +import io.cloudbeaver.model.app.ServletAppConfiguration; import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebSessionAuthProcessor; -import io.cloudbeaver.model.user.WebAuthProviderInfo; import io.cloudbeaver.model.user.WebUser; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; import io.cloudbeaver.registry.WebMetaParametersRegistry; import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.service.auth.DBWServiceAuth; -import io.cloudbeaver.service.auth.WebAuthStatus; -import io.cloudbeaver.service.auth.WebUserInfo; +import io.cloudbeaver.service.auth.*; +import io.cloudbeaver.service.auth.model.user.WebAuthProviderInfo; import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.utils.ServletAppUtils; +import jakarta.servlet.http.HttpServletRequest; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; import org.jkiss.dbeaver.model.auth.SMAuthStatus; +import org.jkiss.dbeaver.model.auth.SMSessionExternal; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; +import org.jkiss.dbeaver.model.security.SMConstants; import org.jkiss.dbeaver.model.security.SMController; import org.jkiss.dbeaver.model.security.SMSubjectType; +import org.jkiss.dbeaver.model.security.exception.SMTooManySessionsException; import org.jkiss.dbeaver.model.security.user.SMUser; import org.jkiss.utils.CommonUtils; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -63,31 +70,14 @@ public WebAuthStatus authLogin( @NotNull String providerId, @Nullable String providerConfigurationId, @Nullable Map authParameters, - boolean linkWithActiveUser + boolean linkWithActiveUser, + boolean forceSessionsLogout ) throws DBWebException { - if (CommonUtils.isEmpty(providerId)) { - throw new DBWebException("Missing auth provider parameter"); - } - if (authParameters == null) { - authParameters = Map.of(); - } - SMController securityController = webSession.getSecurityController(); - String currentSmSessionId = (webSession.getUser() == null || CBApplication.getInstance().isConfigurationMode()) - ? null - : webSession.getUserContext().getSmSessionId(); - try { - var smAuthInfo = securityController.authenticate( - webSession.getSessionId(), - currentSmSessionId, - webSession.getSessionParameters(), - WebSession.CB_SESSION_TYPE, - providerId, - providerConfigurationId, - authParameters - ); - - linkWithActiveUser = linkWithActiveUser && CBApplication.getInstance().getAppConfiguration().isLinkExternalCredentialsWithUser(); + var smAuthInfo = initiateAuthentication(webSession, providerId, providerConfigurationId, authParameters, forceSessionsLogout); + //TODO deprecated, use asyncAuthLogin for federated auth, exits for backward compatibility + linkWithActiveUser = linkWithActiveUser && CBApplication.getInstance().getAppConfiguration() + .isLinkExternalCredentialsWithUser(); if (smAuthInfo.getAuthStatus() == SMAuthStatus.IN_PROGRESS) { //run async auth process return new WebAuthStatus(smAuthInfo.getAuthAttemptId(), smAuthInfo.getRedirectUrl(), smAuthInfo.getAuthStatus()); @@ -96,10 +86,104 @@ public WebAuthStatus authLogin( var authProcessor = new WebSessionAuthProcessor(webSession, smAuthInfo, linkWithActiveUser); return new WebAuthStatus(smAuthInfo.getAuthStatus(), authProcessor.authenticateSession()); } + } catch (SMTooManySessionsException e) { + throw new DBWebException("User authentication failed", e.getErrorType(), e); } catch (Exception e) { throw new DBWebException("User authentication failed", e); } + } + + @Override + public WebAsyncAuthStatus federatedLogin( + @NotNull HttpServletRequest httpRequest, + @NotNull WebSession webSession, + @NotNull String providerId, + @Nullable String providerConfigurationId, + boolean linkWithActiveUser, + boolean forceSessionsLogout + ) throws DBWebException { + WebAuthProviderDescriptor providerDescriptor = WebAuthProviderRegistry.getInstance().getAuthProvider(providerId); + if (providerDescriptor == null) { + throw new DBWebException("Provider '" + providerId + "' not found"); + } + if (!providerDescriptor.isFederated()) { + throw new DBWebException("Provider '" + providerId + "' is not federated"); + } + try { + Map authParameters = new HashMap<>(); + authParameters.put(SMConstants.USER_ORIGIN, ServletAppUtils.getOriginFromRequest(httpRequest)); + + var smAuthInfo = initiateAuthentication(webSession, providerId, providerConfigurationId, authParameters, forceSessionsLogout); + if (smAuthInfo.getAuthStatus() != SMAuthStatus.IN_PROGRESS) { + throw new DBWebException("Unexpected auth status: " + smAuthInfo.getAuthStatus()); + } + if (CommonUtils.isEmpty(smAuthInfo.getRedirectUrl())) { + throw new DBWebException("Missing redirect URL"); + } + WebAsyncTaskInfo authTask = webSession.createAsyncTask(providerId + " authentication"); + authTask.setRunning(true); + authTask.setJob( + new WebAsyncAuthJob(providerId + " authentication job", smAuthInfo.getAuthAttemptId(), linkWithActiveUser) + ); + return new WebAsyncAuthStatus(smAuthInfo.getRedirectUrl(), authTask); + } catch (SMTooManySessionsException e) { + throw new DBWebException("User authentication failed", e.getErrorType(), e); + } catch (Exception e) { + throw new DBWebException("User authentication failed", e); + } + } + + @Override + public WebAsyncAuthTaskResult federatedAuthTaskResult(@NotNull WebSession webSession, @NotNull String taskId) throws DBWebException { + WebAsyncTaskInfo taskInfo = webSession.asyncTaskStatus(taskId, true); + if (taskInfo == null) { + throw new DBWebException("Task '" + taskId + "' not found"); + } + if (taskInfo.isRunning()) { + throw new DBWebException("Task '" + taskId + "' is running"); + } + if (taskInfo.getJob() == null || !WebAsyncAuthJob.class.isAssignableFrom(taskInfo.getJob().getClass())) { + throw new DBWebException("Task '" + taskId + "' is not async auth task"); + } + WebAsyncAuthJob job = (WebAsyncAuthJob) taskInfo.getJob(); + List userTokens = job.getAuthResult(); + if (CommonUtils.isEmpty(userTokens)) { + userTokens = List.of(); + } + return new WebAsyncAuthTaskResult(userTokens); + } + + private static SMAuthInfo initiateAuthentication( + @NotNull WebSession webSession, + @NotNull String providerId, + @Nullable String providerConfigurationId, + @Nullable Map authParameters, + boolean forceSessionsLogout + ) throws DBException { + if (CommonUtils.isEmpty(providerId)) { + throw new DBWebException("Missing auth provider parameter"); + } + WebAuthProviderDescriptor authProviderDescriptor = WebAuthProviderRegistry.getInstance() + .getAuthProvider(providerId); + if (authProviderDescriptor.isTrusted()) { + throw new DBWebException(authProviderDescriptor.getLabel() + " not allowed for authorization via GQL API"); + } + SMController securityController = webSession.getSecurityController(); + String currentSmSessionId = (webSession.getUser() == null || CBApplication.getInstance().isConfigurationMode()) + ? null + : webSession.getUserContext().getSmSessionId(); + var smAuthInfo = securityController.authenticate( + webSession.getSessionId(), + currentSmSessionId, + webSession.getSessionParameters(), + WebSession.CB_SESSION_TYPE, + providerId, + providerConfigurationId, + authParameters, + forceSessionsLogout + ); + return smAuthInfo; } @Override @@ -114,19 +198,24 @@ public WebAuthStatus authUpdateStatus(@NotNull WebSession webSession, @NotNull S case IN_PROGRESS: return new WebAuthStatus(smAuthInfo.getAuthAttemptId(), smAuthInfo.getRedirectUrl(), smAuthInfo.getAuthStatus()); case ERROR: - throw new DBWebException(smAuthInfo.getError()); + throw new DBWebException(smAuthInfo.getError(), smAuthInfo.getErrorCode()); case EXPIRED: throw new DBException("Authorization has already been processed"); default: throw new DBWebException("Unknown auth status:" + smAuthInfo.getAuthStatus()); } + } catch (DBWebException e) { + throw e; + } catch (SMTooManySessionsException e) { + throw new DBWebException(e.getMessage(), e.getErrorType()); } catch (DBException e) { throw new DBWebException(e.getMessage(), e); } } @Override - public void authLogout( + public WebLogoutInfo authLogout( + @NotNull HttpServletRequest httpRequest, @NotNull WebSession webSession, @Nullable String providerId, @Nullable String configurationId @@ -135,7 +224,38 @@ public void authLogout( throw new DBWebException("Not logged in"); } try { - webSession.removeAuthInfo(providerId); + List removedInfos = webSession.removeAuthInfo(providerId); + List logoutUrls = new ArrayList<>(); + var cbApp = CBApplication.getInstance(); + String origin = ServletAppUtils.getOriginFromRequest(httpRequest); + for (WebAuthInfo removedInfo : removedInfos) { + if (removedInfo.getAuthProviderDescriptor() + .getInstance() instanceof SMSignOutLinkProvider provider + && removedInfo.getAuthSession() != null + ) { + var providerConfig = + cbApp.getAuthConfiguration().getAuthProviderConfiguration(removedInfo.getAuthConfiguration()); + if (providerConfig == null) { + log.warn(removedInfo.getAuthConfiguration() + " provider configuration wasn't found"); + continue; + } + String logoutUrl; + + if (removedInfo.getAuthSession() instanceof SMSessionExternal externalSession) { + logoutUrl = provider.getUserSignOutLink(providerConfig, + externalSession.getAuthParameters(), origin + ); + } else { + logoutUrl = provider.getUserSignOutLink(providerConfig, + Map.of(), origin + ); + } + if (CommonUtils.isNotEmpty(logoutUrl)) { + logoutUrls.add(logoutUrl); + } + } + } + return new WebLogoutInfo(logoutUrls); } catch (DBException e) { throw new DBWebException("User logout failed", e); } @@ -144,7 +264,12 @@ public void authLogout( @Override public WebUserInfo activeUser(@NotNull WebSession webSession) throws DBWebException { if (webSession.getUser() == null) { - return null; + ServletAppConfiguration appConfiguration = webSession.getApplication().getAppConfiguration(); + if (!appConfiguration.isAnonymousAccessEnabled()) { + return null; + } + SMUser anonymous = new SMUser("anonymous", true, null); + return new WebUserInfo(webSession, new WebUser(anonymous)); } try { // Read user from security controller. It will also read meta parameters @@ -158,7 +283,7 @@ public WebUserInfo activeUser(@NotNull WebSession webSession) throws DBWebExcept return new WebUserInfo(webSession, webSession.getUser()); } } catch (DBException e) { - if (SMUtils.isTokenExpiredExceptionWasHandled(e)) { + if (SMUtils.isRefreshTokenExpiredExceptionWasHandled(e)) { try { webSession.resetUserState(); return null; @@ -171,9 +296,11 @@ public WebUserInfo activeUser(@NotNull WebSession webSession) throws DBWebExcept } @Override - public WebAuthProviderInfo[] getAuthProviders() { + public WebAuthProviderInfo[] getAuthProviders(@NotNull HttpServletRequest request) throws DBWebException { + String origin = ServletAppUtils.getOriginFromRequest(request); return WebAuthProviderRegistry.getInstance().getAuthProviders() - .stream().map(WebAuthProviderInfo::new) + .stream() + .map(descriptor -> new WebAuthProviderInfo(descriptor, origin)) .toArray(WebAuthProviderInfo[]::new); } @@ -213,14 +340,42 @@ public boolean setUserConfigurationParameter( @NotNull WebSession webSession, @NotNull String name, @Nullable String value + ) throws DBWebException { + if (webSession.getUser() == null) { + throw new DBWebException("Preferences cannot be changed for anonymous user"); + } + return setPreference(webSession, name, value); + } + + private static boolean setPreference( + @NotNull WebSession webSession, + @NotNull String name, + @Nullable Object value ) throws DBWebException { webSession.addInfoMessage("Set user parameter - " + name); try { - webSession.getSecurityController().setCurrentUserParameter(name, value); + var params = new HashMap(); + params.put(name, value); + webSession.getUserContext().getPreferenceStore().updatePreferenceValues(params); return true; } catch (DBException e) { throw new DBWebException("Error setting user parameter", e); } } + @Override + public WebUserInfo setUserConfigurationParameters( + @NotNull WebSession webSession, + @NotNull Map parameters + ) throws DBWebException { + if (webSession.getUser() == null) { + throw new DBWebException("Preferences cannot be changed for anonymous user"); + } + try { + webSession.getUserContext().getPreferenceStore().updatePreferenceValues(parameters); + return new WebUserInfo(webSession, webSession.getUser()); + } catch (DBException e) { + throw new DBWebException("Error setting user parameters", e); + } + } } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java index 8aa6f8a2d4..4771f6a417 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,20 +17,22 @@ package io.cloudbeaver.service.auth.local; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.server.actions.AbstractActionServletHandler; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.utils.ServletAppUtils; +import jakarta.servlet.Servlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import javax.servlet.Servlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class LocalServletHandler extends AbstractActionServletHandler { public static final String URI_PREFIX = "open"; + public static final String PARAM_PROJECT_ID = "project_id"; public static final String PARAM_CONNECTION_ID = "id"; public static final String PARAM_CONNECTION_NAME = "name"; public static final String PARAM_CONNECTION_URL = "url"; @@ -39,9 +41,9 @@ public class LocalServletHandler extends AbstractActionServletHandler { @Override public boolean handleRequest(Servlet servlet, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException { - if (URI_PREFIX.equals(WebAppUtils.removeSideSlashes(request.getPathInfo()))) { + if (URI_PREFIX.equals(ServletAppUtils.removeSideSlashes(request.getServletPath()))) { try { - WebSession webSession = CBPlatform.getInstance().getSessionManager().getWebSession(request, response, true); + WebSession webSession = CBApplication.getInstance().getSessionManager().getWebSession(request, response, true); createActionFromParams(webSession, request, response); return true; } catch (Exception e) { diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalSessionHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalSessionHandler.java index cf1969884d..c2261f8e5f 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalSessionHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalSessionHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -20,14 +20,12 @@ import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.actions.CBServerAction; import io.cloudbeaver.server.actions.AbstractActionSessionHandler; +import io.cloudbeaver.server.actions.CBServerAction; import org.jkiss.dbeaver.DBException; import java.io.IOException; -import java.util.List; -import java.util.function.Predicate; -import java.util.stream.Collectors; +import java.util.stream.Stream; /** * LocalSessionHandler @@ -52,29 +50,21 @@ protected String getActionConsole() { @Override protected void openDatabaseConsole(WebSession webSession, CBServerAction action) throws DBException { + String projectId = action.getParameter(LocalServletHandler.PARAM_PROJECT_ID); String connectionId = action.getParameter(LocalServletHandler.PARAM_CONNECTION_ID); String connectionName = action.getParameter(LocalServletHandler.PARAM_CONNECTION_NAME); String connectionURL = action.getParameter(LocalServletHandler.PARAM_CONNECTION_URL); - WebConnectionInfo connectionInfo = null; + Stream stream = webSession.getAccessibleProjects().stream() + .filter(c -> projectId == null || c.getId().equals(projectId)) + .flatMap(p -> p.getConnections().stream()); if (connectionId != null) { - connectionInfo = webSession.findWebConnectionInfo(connectionId); + stream = stream.filter(c -> c.getId().equals(connectionId)); } else if (connectionName != null) { - connectionInfo = findConnection(webSession, t -> t.getName().equals(connectionName)); + stream = stream.filter(t -> t.getName().equals(connectionName)); } else if (connectionURL != null) { - connectionInfo = findConnection(webSession, t -> t.getUrl().equals(connectionURL)); - } - if (connectionInfo == null) { - throw new DBException("Connection is not found in the session"); + stream = stream.filter(t -> t.getUrl().equals(connectionURL)); } + WebConnectionInfo connectionInfo = stream.findFirst().orElseThrow(() -> new DBException("Connection is not found in the session")); WebServiceUtils.fireActionParametersOpenEditor(webSession, connectionInfo.getDataSourceContainer(), false); } - - private WebConnectionInfo findConnection(WebSession webSession, Predicate filter) { - List filteredConnections = webSession.getConnections().stream().filter(filter).collect(Collectors.toList()); - if (filteredConnections.size() != 1) { - return null; - } - return filteredConnections.get(0); - - } } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/model/user/WebAuthProviderInfo.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/model/user/WebAuthProviderInfo.java new file mode 100644 index 0000000000..3f8899465c --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/model/user/WebAuthProviderInfo.java @@ -0,0 +1,138 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.auth.model.user; + +import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.auth.SMAuthProviderFederated; +import io.cloudbeaver.auth.provisioning.SMProvisioner; +import io.cloudbeaver.model.app.ServletAuthConfiguration; +import io.cloudbeaver.registry.WebAuthProviderConfiguration; +import io.cloudbeaver.registry.WebAuthProviderDescriptor; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.WebAppUtils; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.security.SMAuthCredentialsProfile; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; + +import java.util.ArrayList; +import java.util.List; + +/** + * WebAuthProviderInfo. + */ +public class WebAuthProviderInfo { + + private static final Log log = Log.getLog(WebAuthProviderInfo.class); + private static final SMAuthProviderCustomConfiguration TEMPLATE_CONFIG = new SMAuthProviderCustomConfiguration("{configuration_id}"); + + @NotNull + private final WebAuthProviderDescriptor descriptor; + private final String origin; + + public WebAuthProviderInfo(@NotNull WebAuthProviderDescriptor descriptor, String origin) { + this.descriptor = descriptor; + this.origin = origin; + } + + public String getId() { + return descriptor.getId(); + } + + public String getLabel() { + return descriptor.getLabel(); + } + + public String getIcon() { + return WebServiceUtils.makeIconId(descriptor.getIcon()); + } + + public String getDescription() { + return descriptor.getDescription(); + } + + public boolean isDefaultProvider() { + if (WebAppUtils.getWebApplication().getAppConfiguration() instanceof ServletAuthConfiguration authConfiguration) { + return descriptor.getId().equals(authConfiguration.getDefaultAuthProvider()); + } + return false; + } + + public boolean isConfigurable() { + return descriptor.isConfigurable(); + } + + public boolean isFederated() { + return descriptor.getInstance() instanceof SMAuthProviderFederated; + } + + public boolean isTrusted() { + return descriptor.isTrusted(); + } + + public boolean isPrivate() { + return descriptor.isPrivate(); + } + + public boolean isRequired() { + return descriptor.isRequired(); + } + + public boolean isAuthHidden() { + return descriptor.isAuthHidden(); + } + + public boolean isAuthRoleProvided(SMAuthProviderCustomConfiguration configuration) { + if (descriptor.getInstance() instanceof SMProvisioner provisioner) { + return provisioner.isAuthRoleProvided(configuration); + } + return false; + } + + public boolean isSupportProvisioning() { + return descriptor.getInstance() instanceof SMProvisioner; + } + + public List getConfigurations() { + List result = new ArrayList<>(); + for (SMAuthProviderCustomConfiguration cfg : CBApplication.getInstance().getAppConfiguration().getAuthCustomConfigurations()) { + if (!cfg.isDisabled() && getId().equals(cfg.getProvider())) { + result.add(new WebAuthProviderConfiguration(descriptor, cfg, origin)); + } + } + return result; + } + + public List getCredentialProfiles() { + return descriptor.getCredentialProfiles(); + } + + public String[] getRequiredFeatures() { + String[] rf = descriptor.getRequiredFeatures(); + return rf == null ? new String[0] : rf; + } + + public WebAuthProviderConfiguration getTemplateConfiguration() { + return new WebAuthProviderConfiguration(descriptor, TEMPLATE_CONFIG, origin); + } + + @Override + public String toString() { + return getLabel(); + } + +} diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF index bd43ec731d..4137656a04 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF @@ -3,12 +3,11 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Data Transfer Bundle-SymbolicName: io.cloudbeaver.service.data.transfer;singleton:=true -Bundle-Version: 1.0.83.qualifier -Bundle-Release-Date: 20230209 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 1.0.130.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy -Bundle-ClassPath: . -Require-Bundle: io.cloudbeaver.server, +Require-Bundle: io.cloudbeaver.server.ce, org.jkiss.dbeaver.data.transfer, org.jkiss.dbeaver.registry Automatic-Module-Name: io.cloudbeaver.service.data.transfer diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml b/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml index a313c54c91..551f44d1de 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml +++ b/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.data.transfer - 1.0.83-SNAPSHOT + 1.0.130-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/schema/service.data.transfer.graphqls b/server/bundles/io.cloudbeaver.service.data.transfer/schema/service.data.transfer.graphqls index 608c5ad8c1..6684326f4d 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/schema/service.data.transfer.graphqls +++ b/server/bundles/io.cloudbeaver.service.data.transfer/schema/service.data.transfer.graphqls @@ -19,6 +19,7 @@ input DataTransferOutputSettingsInput { encoding: String timestampPattern: String compress: Boolean + fileName: String } type DataTransferOutputSettings { @@ -34,26 +35,33 @@ type DataTransferDefaultExportSettings { } input DataTransferParameters { - # Processor ID + "Processor ID" processorId: ID! - # General settings: - # - openNewConnection: opens new database connection for data transfer task + """ + General settings: + - openNewConnection: opens new database connection for data transfer task + """ settings: Object - # Processor properties. See DataTransferProcessorInfo.properties + "Processor properties. See DataTransferProcessorInfo.properties" processorProperties: Object! - # Consumer properties. See StreamConsumerSettings + "Consumer properties. See StreamConsumerSettings" outputSettings: DataTransferOutputSettingsInput - # Data filter settings + "Data filter settings" filter: SQLDataFilter } extend type Query { - # Available transfer processors + "Returns available transfer processors for data transfer export" dataTransferAvailableStreamProcessors: [ DataTransferProcessorInfo! ]! + "Returns available transfer processors for data transfer import" + dataTransferAvailableImportStreamProcessors: [ DataTransferProcessorInfo! ]! + + "Returns default export settings for data transfer" dataTransferDefaultExportSettings: DataTransferDefaultExportSettings! + "Creates async task for data transfer export from the container node path" dataTransferExportDataFromContainer( projectId: ID, connectionId: ID!, @@ -61,6 +69,7 @@ extend type Query { parameters: DataTransferParameters! ): AsyncTaskInfo! + "Creates async task for data transfer export from results" dataTransferExportDataFromResults( projectId: ID, connectionId: ID!, @@ -69,6 +78,7 @@ extend type Query { parameters: DataTransferParameters! ): AsyncTaskInfo! - dataTransferRemoveDataFile(dataFileId: String!): Boolean + "Deletes data transfer file by ID" + dataTransferRemoveDataFile(dataFileId: String!): Boolean @deprecated(reason: "25.0.1") } diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/DBWServiceDataTransfer.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/DBWServiceDataTransfer.java index cb6561ef9c..ae38f2a5ee 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/DBWServiceDataTransfer.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/DBWServiceDataTransfer.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,17 +16,25 @@ */ package io.cloudbeaver.service.data.transfer; -import io.cloudbeaver.service.DBWService; import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebAction; import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.service.DBWService; import io.cloudbeaver.service.data.transfer.impl.WebDataTransferDefaultExportSettings; import io.cloudbeaver.service.data.transfer.impl.WebDataTransferParameters; import io.cloudbeaver.service.data.transfer.impl.WebDataTransferStreamProcessor; +import io.cloudbeaver.service.data.transfer.impl.WebDataTransferTaskConfig; import io.cloudbeaver.service.sql.WebSQLContextInfo; import io.cloudbeaver.service.sql.WebSQLProcessor; +import io.cloudbeaver.service.sql.WebSQLResultsInfo; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.rm.RMConstants; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import java.io.OutputStream; +import java.nio.file.Path; import java.util.List; /** @@ -37,6 +45,9 @@ public interface DBWServiceDataTransfer extends DBWService { @WebAction List getAvailableStreamProcessors(WebSession session) throws DBWebException; + @WebAction + List getAvailableImportStreamProcessors(WebSession session) throws DBWebException; + @WebAction WebAsyncTaskInfo dataTransferExportDataFromContainer( WebSQLProcessor sqlProcessor, @@ -44,13 +55,34 @@ WebAsyncTaskInfo dataTransferExportDataFromContainer( WebDataTransferParameters parameters) throws DBWebException; @WebAction + WebAsyncTaskInfo asyncImportDataContainer( + @NotNull String processorId, + @NotNull Path path, + @NotNull WebSQLResultsInfo webSQLResultsInfo, + @NotNull WebSession webSession) throws DBWebException; + + @WebAction(requireGlobalPermissions = RMConstants.GLOBAL_PERMISSION_DATA_EDITOR_EXPORT) WebAsyncTaskInfo dataTransferExportDataFromResults( WebSQLContextInfo sqlContextInfo, String resultsId, WebDataTransferParameters parameters) throws DBWebException; + /** + * It's deprecated because now we use streaming file to response directly, and we don't need to clean up any files + * after data transfer. + */ @WebAction + @Deprecated Boolean dataTransferRemoveDataFile(WebSession session, String dataFileId) throws DBWebException; WebDataTransferDefaultExportSettings defaultExportSettings(); + + /** + * Usefully for exporting directly to http response and avoid to create temp files. + */ + void exportDataTransferToStream( + @NotNull DBRProgressMonitor monitor, + @NotNull WebDataTransferTaskConfig taskConfig, + @NotNull OutputStream outputStream + ) throws DBException; } diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/WebServiceBindingDataTransfer.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/WebServiceBindingDataTransfer.java index 3c173db335..024f52c585 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/WebServiceBindingDataTransfer.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/WebServiceBindingDataTransfer.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -21,6 +21,7 @@ import io.cloudbeaver.service.DBWServiceBindingServlet; import io.cloudbeaver.service.DBWServletContext; import io.cloudbeaver.service.WebServiceBindingBase; +import io.cloudbeaver.service.data.transfer.impl.WebDataTransferImportServlet; import io.cloudbeaver.service.data.transfer.impl.WebDataTransferParameters; import io.cloudbeaver.service.data.transfer.impl.WebDataTransferServlet; import io.cloudbeaver.service.data.transfer.impl.WebServiceDataTransfer; @@ -42,6 +43,8 @@ public void bindWiring(DBWBindingContext model) { model.getQueryType() .dataFetcher("dataTransferAvailableStreamProcessors", env -> getService(env).getAvailableStreamProcessors(getWebSession(env))) + .dataFetcher("dataTransferAvailableImportStreamProcessors", + env -> getService(env).getAvailableImportStreamProcessors(getWebSession(env))) .dataFetcher("dataTransferExportDataFromContainer", env -> getService(env).dataTransferExportDataFromContainer( WebServiceBindingSQL.getSQLProcessor(env), env.getArgument("containerNodePath"), @@ -63,10 +66,18 @@ public void bindWiring(DBWBindingContext model) { @Override public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException { + if (!application.isMultiuser()) { + return; + } servletContext.addServlet( "dataTransfer", new WebDataTransferServlet(application, getServiceImpl()), application.getServicesURI() + "data/*" ); + servletContext.addServlet( + "dataTransferImport", + new WebDataTransferImportServlet(application, getServiceImpl()), + application.getServicesURI() + "data/import/*" + ); } } diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferDefaultExportSettings.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferDefaultExportSettings.java index 4e35ab0ecd..decb1a96fe 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferDefaultExportSettings.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferDefaultExportSettings.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -31,7 +31,8 @@ public WebDataTransferDefaultExportSettings() { false, defConsumerSettings.getOutputEncoding(), defConsumerSettings.getOutputTimestampPattern(), - defConsumerSettings.isCompressResults() + defConsumerSettings.isCompressResults(), + null ); this.supportedEncodings = Charset.availableCharsets().keySet(); } diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferImportServlet.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferImportServlet.java new file mode 100644 index 0000000000..82736f91f7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferImportServlet.java @@ -0,0 +1,132 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.data.transfer.impl; + +import com.google.gson.stream.JsonWriter; +import io.cloudbeaver.DBWConstants; +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.WebAsyncTaskInfo; +import io.cloudbeaver.model.WebConnectionInfo; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.BaseWebPlatform; +import io.cloudbeaver.server.CBConstants; +import io.cloudbeaver.server.WebAppUtils; +import io.cloudbeaver.server.WebApplication; +import io.cloudbeaver.service.WebServiceServletBase; +import io.cloudbeaver.service.data.transfer.DBWServiceDataTransfer; +import io.cloudbeaver.service.sql.WebSQLContextInfo; +import io.cloudbeaver.service.sql.WebSQLProcessor; +import io.cloudbeaver.service.sql.WebSQLResultsInfo; +import io.cloudbeaver.service.sql.WebServiceBindingSQL; +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.MultipartConfig; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.data.json.JSONUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +@MultipartConfig +public class WebDataTransferImportServlet extends WebServiceServletBase { + + private static final Log log = Log.getLog(WebDataTransferImportServlet.class); + public static final String ECLIPSE_JETTY_MULTIPART_CONFIG = "org.eclipse.jetty.multipartConfig"; + + DBWServiceDataTransfer dbwServiceDataTransfer; + + + public WebDataTransferImportServlet(WebApplication application, DBWServiceDataTransfer dbwServiceDataTransfer) { + super(application); + this.dbwServiceDataTransfer = dbwServiceDataTransfer; + } + + @Override + protected void processServiceRequest( + WebSession session, + HttpServletRequest request, + HttpServletResponse response + ) throws IOException, DBWebException { + if (!session.isAuthorizedInSecurityManager()) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Import for users only"); + return; + } + if (!session.hasPermission(DBWConstants.GLOBAL_PERMISSION_DATA_EDITOR_IMPORT)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Import is not allowed for this user"); + return; + } + if ("POST".equalsIgnoreCase(request.getMethod())) { + Path tempFolder = WebAppUtils.getWebPlatform().getTempFolder(session.getProgressMonitor(), + BaseWebPlatform.TEMP_FILE_IMPORT_FOLDER); + MultipartConfigElement MULTI_PART_CONFIG = new MultipartConfigElement(tempFolder.toString()); + + request.setAttribute(ECLIPSE_JETTY_MULTIPART_CONFIG, MULTI_PART_CONFIG); + + Map variables = getVariables(request); + + String projectId = JSONUtils.getString(variables, "projectId"); + String connectionId = JSONUtils.getString(variables, "connectionId"); + String contextId = JSONUtils.getString(variables, "contextId"); + String resultId = JSONUtils.getString(variables, "resultsId"); + String processorId = JSONUtils.getString(variables, "processorId"); + + if (projectId == null || connectionId == null || contextId == null || resultId == null || processorId == null) { + throw new IllegalArgumentException("Missing required parameters"); + } + + WebConnectionInfo webConnectionInfo = session.getAccessibleProjectById(projectId).getWebConnectionInfo(connectionId); + WebSQLProcessor processor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); + WebSQLContextInfo webSQLContextInfo = processor.getContext(contextId); + + if (webSQLContextInfo == null) { + throw new DBWebException("Context is empty"); + } + + WebSQLResultsInfo webSQLResultsInfo = webSQLContextInfo.getResults(resultId); + Path filePath; + + try { + InputStream file = request.getPart("fileData").getInputStream(); + filePath = tempFolder.resolve(UUID.randomUUID().toString()); + Files.write(filePath, file.readAllBytes()); + } catch (ServletException e) { + throw new DBWebException(e.getMessage()); + } + + WebAsyncTaskInfo asyncImportDataContainer = + dbwServiceDataTransfer.asyncImportDataContainer(processorId, filePath, webSQLResultsInfo, session); + response.setContentType(CBConstants.APPLICATION_JSON); + Map parameters = new LinkedHashMap<>(); + parameters.put("id", asyncImportDataContainer.getId()); + parameters.put("name", asyncImportDataContainer.getName()); + parameters.put("running", asyncImportDataContainer.isRunning()); + parameters.put("status", asyncImportDataContainer.getStatus()); + parameters.put("error", asyncImportDataContainer.getError()); + parameters.put("taskResult", asyncImportDataContainer.getTaskResult()); + try (JsonWriter writer = new JsonWriter(response.getWriter())) { + JSONUtils.serializeMap(writer, parameters); + } + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferOutputSettings.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferOutputSettings.java index 9d05a280d9..7c288644b6 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferOutputSettings.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferOutputSettings.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -25,19 +25,22 @@ public class WebDataTransferOutputSettings { private final String encoding; private final String timestampPattern; private final boolean compress; + private final String fileName; public WebDataTransferOutputSettings(Map outputSettings) { this.insertBom = JSONUtils.getBoolean(outputSettings, "insertBom", false); this.encoding = JSONUtils.getString(outputSettings, "encoding"); this.timestampPattern = JSONUtils.getString(outputSettings, "timestampPattern"); this.compress = JSONUtils.getBoolean(outputSettings, "compress", false); + this.fileName = JSONUtils.getString(outputSettings, "fileName"); } - public WebDataTransferOutputSettings(boolean insertBom, String encoding, String timestampPattern, boolean compress) { + public WebDataTransferOutputSettings(boolean insertBom, String encoding, String timestampPattern, boolean compress, String fileName) { this.insertBom = insertBom; this.encoding = encoding; this.timestampPattern = timestampPattern; this.compress = compress; + this.fileName = fileName; } public boolean isInsertBom() { @@ -55,4 +58,8 @@ public String getTimestampPattern() { public boolean isCompress() { return compress; } + + public String getFileName() { + return fileName; + } } diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferParameters.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferParameters.java index ea1e855f5e..ab35121bc8 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferParameters.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferParameters.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferServlet.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferServlet.java index 8f180fcdee..445379e8b3 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferServlet.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferServlet.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -18,22 +18,18 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.WebApplication; import io.cloudbeaver.service.WebServiceServletBase; import io.cloudbeaver.service.data.transfer.DBWServiceDataTransfer; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.tools.transfer.registry.DataTransferProcessorDescriptor; import org.jkiss.dbeaver.tools.transfer.registry.DataTransferRegistry; import org.jkiss.utils.CommonUtils; -import org.jkiss.utils.IOUtils; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; public class WebDataTransferServlet extends WebServiceServletBase { @@ -41,7 +37,7 @@ public class WebDataTransferServlet extends WebServiceServletBase { private final DBWServiceDataTransfer dtManager; - public WebDataTransferServlet(CBApplication application, DBWServiceDataTransfer dtManager) { + public WebDataTransferServlet(WebApplication application, DBWServiceDataTransfer dtManager) { super(application); this.dtManager = dtManager; } @@ -67,25 +63,15 @@ protected void processServiceRequest(WebSession session, HttpServletRequest requ } String fileName = taskInfo.getExportFileName(); if (!CommonUtils.isEmpty(fileName)) { - fileName += "." + WebDataTransferUtils.getProcessorFileExtension(processor); + fileName += "." + WebDataTransferUtils.getProcessorFileExtension(processor, taskInfo.getParameters().getProcessorProperties()); } else { fileName = taskInfo.getDataFileId(); } fileName = WebDataTransferUtils.normalizeFileName(fileName, taskInfo.getParameters().getOutputSettings()); - Path dataFile = taskInfo.getDataFile(); session.addInfoMessage("Download data ..."); response.setHeader("Content-Type", processor.getContentType()); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); - response.setHeader("Content-Length", String.valueOf(Files.size(dataFile))); - try (InputStream is = Files.newInputStream(dataFile)) { - IOUtils.copyStream(is, response.getOutputStream()); - } - - // TODO: cleanup export files ASAP? - if (false) { - dtConfig.removeTask(taskInfo); - } + dtManager.exportDataTransferToStream(session.getProgressMonitor(), taskInfo, response.getOutputStream()); } - } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferSessionConfig.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferSessionConfig.java index 4abfbdbeb8..7196ecd4b1 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferSessionConfig.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferSessionConfig.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -36,16 +36,8 @@ public void addTask(WebDataTransferTaskConfig taskConfig) { } } - public void removeTask(WebDataTransferTaskConfig taskConfig) { - synchronized (tasks) { - tasks.remove(taskConfig.getDataFileId()); - taskConfig.deleteFile(); - } - } - public WebDataTransferSessionConfig deleteExportFiles() { synchronized (tasks) { - tasks.values().forEach(WebDataTransferTaskConfig::deleteFile); tasks.clear(); } return this; diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferStreamProcessor.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferStreamProcessor.java index d006688829..64a0c62cb8 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferStreamProcessor.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferStreamProcessor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferTaskConfig.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferTaskConfig.java index 25aad20867..d2dd15d269 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferTaskConfig.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferTaskConfig.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,31 +16,36 @@ */ package io.cloudbeaver.service.data.transfer.impl; +import io.cloudbeaver.service.sql.WebSQLResultsInfo; import org.jkiss.dbeaver.Log; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; +import org.jkiss.dbeaver.model.struct.DBSDataContainer; public class WebDataTransferTaskConfig { private static final Log log = Log.getLog(WebDataTransferTaskConfig.class); - private Path dataFile; + private String fileNameKey; private WebDataTransferParameters parameters; private String exportFileName; + private DBSDataContainer dataContainer; + private WebSQLResultsInfo resultsInfo; - public WebDataTransferTaskConfig(Path dataFile, WebDataTransferParameters parameters) { - this.dataFile = dataFile; + public WebDataTransferTaskConfig( + String fileNameKey, + WebDataTransferParameters parameters, + String exportFileName, + DBSDataContainer dataContainer, + WebSQLResultsInfo webSQLResultsInfo + ) { + this.fileNameKey = fileNameKey; this.parameters = parameters; - } - - public Path getDataFile() { - return dataFile; + this.exportFileName = exportFileName; + this.dataContainer = dataContainer; + this.resultsInfo = webSQLResultsInfo; } public String getDataFileId() { - return dataFile.getFileName().toString(); + return fileNameKey; } public WebDataTransferParameters getParameters() { @@ -51,15 +56,11 @@ public String getExportFileName() { return exportFileName; } - public void setExportFileName(String exportFileName) { - this.exportFileName = exportFileName; + public DBSDataContainer getDataContainer() { + return dataContainer; } - public void deleteFile() { - try { - Files.delete(dataFile); - } catch (IOException e) { - log.error("Error deleting export file " + dataFile.toAbsolutePath(), e); - } + public WebSQLResultsInfo getResultsInfo() { + return resultsInfo; } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferUtils.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferUtils.java index 09b7b6bbaf..d05129736a 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferUtils.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferUtils.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -23,9 +23,12 @@ import org.jkiss.dbeaver.tools.transfer.registry.DataTransferProcessorDescriptor; import org.jkiss.utils.CommonUtils; +import java.util.Map; + class WebDataTransferUtils { private static final Log log = Log.getLog(WebDataTransferUtils.class); + public static final String EXTENSION = "extension"; public static String getProcessorFileExtension(DataTransferProcessorDescriptor processor) { @@ -34,6 +37,14 @@ public static String getProcessorFileExtension(DataTransferProcessorDescriptor p return CommonUtils.isEmpty(ext) ? "data" : ext; } + public static String getProcessorFileExtension(DataTransferProcessorDescriptor processor, Map processorProperties) { + if (processorProperties != null && processorProperties.get(EXTENSION) != null) { + return CommonUtils.toString(processorProperties.get(EXTENSION), "data"); + } + + return getProcessorFileExtension(processor); + } + public static String normalizeFileName( @NotNull String fileName, @NotNull WebDataTransferOutputSettings outputSettings diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java index b350b62fa1..8b0fedb330 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -20,7 +20,6 @@ import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.session.WebAsyncTaskProcessor; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.data.transfer.DBWServiceDataTransfer; import io.cloudbeaver.service.sql.WebSQLContextInfo; @@ -30,28 +29,24 @@ import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.exec.DBCException; -import org.jkiss.dbeaver.model.exec.DBCResultSet; -import org.jkiss.dbeaver.model.exec.DBCSession; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; -import org.jkiss.dbeaver.model.sql.DBQuotaException; import org.jkiss.dbeaver.model.struct.DBSDataContainer; +import org.jkiss.dbeaver.model.struct.DBSDataManipulator; import org.jkiss.dbeaver.model.struct.DBSEntity; +import org.jkiss.dbeaver.model.struct.DBSObjectContainer; import org.jkiss.dbeaver.tools.transfer.IDataTransferConsumer; import org.jkiss.dbeaver.tools.transfer.IDataTransferProcessor; -import org.jkiss.dbeaver.tools.transfer.database.DatabaseProducerSettings; -import org.jkiss.dbeaver.tools.transfer.database.DatabaseTransferProducer; +import org.jkiss.dbeaver.tools.transfer.database.*; import org.jkiss.dbeaver.tools.transfer.registry.DataTransferProcessorDescriptor; import org.jkiss.dbeaver.tools.transfer.registry.DataTransferRegistry; -import org.jkiss.dbeaver.tools.transfer.stream.IStreamDataExporter; -import org.jkiss.dbeaver.tools.transfer.stream.StreamConsumerSettings; -import org.jkiss.dbeaver.tools.transfer.stream.StreamTransferConsumer; +import org.jkiss.dbeaver.tools.transfer.stream.*; import org.jkiss.dbeaver.utils.ContentUtils; import org.jkiss.utils.CommonUtils; import java.io.IOException; +import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.nio.file.Files; import java.nio.file.Path; @@ -63,8 +58,6 @@ */ public class WebServiceDataTransfer implements DBWServiceDataTransfer { - public static final String QUOTA_PROP_FILE_LIMIT = "dataExportFileSizeLimit"; - private static final Log log = Log.getLog(WebServiceDataTransfer.class); private final Path dataExportFolder; @@ -82,7 +75,19 @@ public WebServiceDataTransfer() { @Override public List getAvailableStreamProcessors(WebSession session) { - List processors = DataTransferRegistry.getInstance().getAvailableProcessors(StreamTransferConsumer.class, DBSEntity.class); + List processors = DataTransferRegistry.getInstance() + .getAvailableProcessors(StreamTransferConsumer.class, DBSEntity.class); + if (CommonUtils.isEmpty(processors)) { + return Collections.emptyList(); + } + + return processors.stream().map(x -> new WebDataTransferStreamProcessor(session, x)).collect(Collectors.toList()); + } + + @Override + public List getAvailableImportStreamProcessors(WebSession session) { + List processors = + DataTransferRegistry.getInstance().getAvailableProcessors(StreamTransferProducer.class, DBSEntity.class); if (CommonUtils.isEmpty(processors)) { return Collections.emptyList(); } @@ -107,7 +112,15 @@ public WebAsyncTaskInfo dataTransferExportDataFromContainer( } @NotNull - private String makeUniqueFileName(WebSQLProcessor sqlProcessor, DataTransferProcessorDescriptor processor) { + private String makeUniqueFileName( + WebSQLProcessor sqlProcessor, + DataTransferProcessorDescriptor processor, + Map processorProperties + ) { + if (processorProperties != null && processorProperties.get(StreamConsumerSettings.PROP_FILE_EXTENSION) != null) { + return sqlProcessor.getWebSession().getSessionId() + "_" + UUID.randomUUID() + + "." + processorProperties.get(StreamConsumerSettings.PROP_FILE_EXTENSION); + } return sqlProcessor.getWebSession().getSessionId() + "_" + UUID.randomUUID() + "." + WebDataTransferUtils.getProcessorFileExtension(processor); } @@ -123,74 +136,105 @@ public WebAsyncTaskInfo dataTransferExportDataFromResults( } @Override - public Boolean dataTransferRemoveDataFile(WebSession webSession, String dataFileId) throws DBWebException { - WebDataTransferSessionConfig dtConfig = WebDataTransferUtils.getSessionDataTransferConfig(webSession); - WebDataTransferTaskConfig taskInfo = dtConfig.getTask(dataFileId); - if (taskInfo == null) { - throw new DBWebException("Session task '" + dataFileId + "' not found"); - } - Path dataFile = taskInfo.getDataFile(); - if (dataFile != null) { - try { - Files.delete(dataFile); - } catch (IOException e) { - log.warn("Error deleting data file '" + dataFile.toAbsolutePath() + "'", e); - } - } - dtConfig.removeTask(taskInfo); + public WebDataTransferDefaultExportSettings defaultExportSettings() { + return new WebDataTransferDefaultExportSettings(); + } + @Override + @Deprecated + public Boolean dataTransferRemoveDataFile(WebSession webSession, String dataFileId) throws DBWebException { + //deprecated return true; } @Override - public WebDataTransferDefaultExportSettings defaultExportSettings() { - return new WebDataTransferDefaultExportSettings(); + public void exportDataTransferToStream( + @NotNull DBRProgressMonitor monitor, + @NotNull WebDataTransferTaskConfig taskConfig, + @NotNull OutputStream outputStream + ) throws DBException { + + WebDataTransferParameters parameters = taskConfig.getParameters(); + DBSDataContainer dataContainer = taskConfig.getDataContainer(); + WebSQLResultsInfo resultsInfo = taskConfig.getResultsInfo(); + DataTransferProcessorDescriptor processor = DataTransferRegistry.getInstance().getProcessor(parameters.getProcessorId()); + + try { + exportData(monitor, processor, dataContainer, parameters, resultsInfo, outputStream); + } catch (Exception e) { + throw new DBException("Error exporting data", e); + } } - private WebAsyncTaskInfo asyncExportFromDataContainer(WebSQLProcessor sqlProcessor, WebDataTransferParameters parameters, DBSDataContainer dataContainer, - @Nullable WebSQLResultsInfo resultsInfo) { + private WebAsyncTaskInfo asyncExportFromDataContainer( + @NotNull WebSQLProcessor sqlProcessor, + @NotNull WebDataTransferParameters parameters, + @NotNull DBSDataContainer dataContainer, + @Nullable WebSQLResultsInfo resultsInfo + ) { sqlProcessor.getWebSession().addInfoMessage("Export data"); + log.info(String.format("Data export started: [userId=%s]", sqlProcessor.getWebSession().getUserId())); + DataTransferProcessorDescriptor processor = DataTransferRegistry.getInstance().getProcessor(parameters.getProcessorId()); - WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor() { + String uniqueFileName = makeUniqueFileName(sqlProcessor, processor, parameters.getProcessorProperties()); + var outputSettings = parameters.getOutputSettings(); + String fileNameKey = WebDataTransferUtils.normalizeFileName(uniqueFileName, outputSettings); + String exportFileName = CommonUtils.isEmpty(outputSettings.getFileName()) + ? CommonUtils.escapeFileName(CommonUtils.truncateString(dataContainer.getName(), 32)) + : outputSettings.getFileName(); + WebDataTransferTaskConfig taskConfig = new WebDataTransferTaskConfig( + fileNameKey, parameters, exportFileName, dataContainer, resultsInfo); + + WebDataTransferUtils.getSessionDataTransferConfig(sqlProcessor.getWebSession()) + .addTask(taskConfig); + + //fixme fake task for keeping api + return sqlProcessor.getWebSession().createAndRunAsyncTask( + "Data export", new WebAsyncTaskProcessor<>() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException { + result = fileNameKey; + } + } + ); + } + + public WebAsyncTaskInfo asyncImportDataContainer(@NotNull String processorId, + @NotNull Path path, + @NotNull WebSQLResultsInfo sqlContext, + @NotNull WebSession webSession) throws DBWebException { + webSession.addInfoMessage("Import data"); + DataTransferProcessorDescriptor processor = DataTransferRegistry.getInstance().getProcessor(processorId); + + log.info(String.format("Data import started: [userId=%s]", webSession.getUserId())); + DBSDataContainer dataContainer = sqlContext.getDataContainer(); + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { @Override public void run(DBRProgressMonitor monitor) throws InvocationTargetException { - monitor.beginTask("Export data", 1); + monitor.beginTask("Import data", 1); try { - monitor.subTask("Export data using " + processor.getName()); - Path exportFile = dataExportFolder.resolve(makeUniqueFileName(sqlProcessor, processor)); + monitor.subTask("Import data using " + processor.getName()); try { - exportData(monitor, processor, dataContainer, parameters, resultsInfo, exportFile); + importData(monitor, processor, (DBSDataManipulator) dataContainer, path); } catch (Exception e) { - if (Files.exists(exportFile)) { - try { - Files.delete(exportFile); - } catch (IOException ex) { - log.error("Error deleting export file " + exportFile.toAbsolutePath(), e); - } - } if (e instanceof DBException) { throw e; } - throw new DBException("Error exporting data", e); + throw new DBException("Error importing data", e); } - Path finallyExportFile = parameters.getOutputSettings().isCompress() - ? exportFile.resolveSibling(WebDataTransferUtils.normalizeFileName( - exportFile.getFileName().toString(), parameters.getOutputSettings())) - : exportFile; - WebDataTransferTaskConfig taskConfig = new WebDataTransferTaskConfig(finallyExportFile, parameters); - String exportFileName = CommonUtils.escapeFileName(CommonUtils.truncateString(dataContainer.getName(), 32)); - taskConfig.setExportFileName(exportFileName); - WebDataTransferUtils.getSessionDataTransferConfig(sqlProcessor.getWebSession()).addTask(taskConfig); - - result = finallyExportFile.getFileName().toString(); } catch (Throwable e) { throw new InvocationTargetException(e); } finally { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + log.error("Failed to delete file: " + e.getMessage(), e); + } monitor.done(); } } }; - return sqlProcessor.getWebSession().createAndRunAsyncTask("Data export", runnable); + return webSession.createAndRunAsyncTask("Data import", runnable); } private void exportData( @@ -199,46 +243,16 @@ private void exportData( DBSDataContainer dataContainer, WebDataTransferParameters parameters, WebSQLResultsInfo resultsInfo, - Path exportFile) throws DBException, IOException - { + OutputStream outputStream + ) throws DBException, IOException { IDataTransferProcessor processorInstance = processor.getInstance(); - if (!(processorInstance instanceof IStreamDataExporter)) { + if (!(processorInstance instanceof IStreamDataExporter exporter)) { throw new DBException("Invalid processor. " + IStreamDataExporter.class.getSimpleName() + " expected"); } - IStreamDataExporter exporter = (IStreamDataExporter) processorInstance; - - Number fileSizeLimit = CBApplication.getInstance().getAppConfiguration().getResourceQuota(QUOTA_PROP_FILE_LIMIT); - - StreamTransferConsumer consumer = new StreamTransferConsumer() { - @Override - public void fetchRow(DBCSession session, DBCResultSet resultSet) throws DBCException { - super.fetchRow(session, resultSet); - if (fileSizeLimit != null && getBytesWritten() > fileSizeLimit.longValue()) { - throw new DBQuotaException( - "Data export quota exceeded", QUOTA_PROP_FILE_LIMIT, fileSizeLimit.longValue(), getBytesWritten()); - } - } - }; - - StreamConsumerSettings settings = new StreamConsumerSettings(); - - settings.setOutputFolder(exportFile.getParent().toAbsolutePath().toString()); - settings.setOutputFilePattern(exportFile.getFileName().toString()); - - WebDataTransferOutputSettings outputSettings = parameters.getOutputSettings(); - settings.setOutputEncodingBOM(outputSettings.isInsertBom()); - settings.setCompressResults(outputSettings.isCompress()); - if (!CommonUtils.isEmpty(outputSettings.getEncoding())) { - settings.setOutputEncoding(outputSettings.getEncoding()); - } - if (!CommonUtils.isEmpty(outputSettings.getTimestampPattern())) { - settings.setOutputTimestampPattern(outputSettings.getTimestampPattern()); - } - - Map properties = new HashMap<>(); Map processorProperties = parameters.getProcessorProperties(); if (processorProperties == null) processorProperties = Collections.emptyMap(); + Map properties = new HashMap<>(); for (DBPPropertyDescriptor prop : processor.getProperties()) { Object propValue = processorProperties.get(CommonUtils.toString(prop.getId())); properties.put(prop.getId(), propValue != null ? propValue : prop.getDefaultValue()); @@ -246,24 +260,87 @@ public void fetchRow(DBCSession session, DBCResultSet resultSet) throws DBCExcep // Remove extension property (we specify file name directly) properties.remove(StreamConsumerSettings.PROP_FILE_EXTENSION); - consumer.initTransfer( - dataContainer, - settings, - new IDataTransferConsumer.TransferParameters(processor.isBinaryFormat(), processor.isHTMLFormat()), - exporter, - properties); - - DatabaseTransferProducer producer = new DatabaseTransferProducer( - dataContainer, - parameters.getFilter() == null ? null : parameters.getFilter().makeDataFilter(resultsInfo)); DatabaseProducerSettings producerSettings = new DatabaseProducerSettings(); producerSettings.setExtractType(DatabaseProducerSettings.ExtractType.SINGLE_QUERY); producerSettings.setQueryRowCount(false); producerSettings.setOpenNewConnections(CommonUtils.getOption(parameters.getDbProducerSettings(), "openNewConnection")); + StreamTransferConsumer consumer = new StreamTransferConsumer(); + StreamConsumerSettings settings = makeStreamConsumerSettings(parameters); + DatabaseTransferProducer producer = new DatabaseTransferProducer( + dataContainer, + parameters.getFilter() == null ? null : parameters.getFilter().makeDataFilter(resultsInfo)); + + consumer.initTransfer( + dataContainer, + settings, + new IDataTransferConsumer.TransferParameters(processor.isBinaryFormat(), processor.isHTMLFormat(), outputStream), + exporter, + properties, + producer.getProject()); producer.transferData(monitor, consumer, null, producerSettings, null); consumer.finishTransfer(monitor, false); } + @NotNull + private StreamConsumerSettings makeStreamConsumerSettings(@NotNull WebDataTransferParameters parameters) { + StreamConsumerSettings settings = new StreamConsumerSettings(); + + WebDataTransferOutputSettings outputSettings = parameters.getOutputSettings(); + settings.setOutputEncodingBOM(outputSettings.isInsertBom()); + settings.setCompressResults(outputSettings.isCompress()); + if (!CommonUtils.isEmpty(outputSettings.getEncoding())) { + settings.setOutputEncoding(outputSettings.getEncoding()); + } + if (!CommonUtils.isEmpty(outputSettings.getTimestampPattern())) { + settings.setOutputTimestampPattern(outputSettings.getTimestampPattern()); + } + return settings; + } + + private void importData( + DBRProgressMonitor monitor, + DataTransferProcessorDescriptor processor, + @NotNull DBSDataManipulator dataContainer, + Path path) throws DBException { + IDataTransferProcessor processorInstance = processor.getInstance(); + + StreamTransferProducer producer; + if (dataContainer.getDataSource() != null) { + producer = new StreamTransferProducer(new StreamEntityMapping(path), processor); + + DatabaseTransferConsumer consumer = new DatabaseTransferConsumer(dataContainer); + DatabaseConsumerSettings databaseConsumerSettings = new DatabaseConsumerSettings(); + databaseConsumerSettings.setContainer((DBSObjectContainer) dataContainer.getDataSource()); + databaseConsumerSettings.setEnableQmLogging(true); + consumer.setSettings(databaseConsumerSettings); + + StreamProducerSettings producerSettings = new StreamProducerSettings(); + Map properties = new HashMap<>(); + for (DBPPropertyDescriptor prop : processor.getProperties()) { + properties.put(prop.getId(), prop.getDefaultValue()); + } + producerSettings.setProcessorProperties(properties); + producerSettings.updateProducerSettingsFromStream( + monitor, + producer, + processorInstance, + properties); + DatabaseMappingContainer databaseMappingContainer = + new DatabaseMappingContainer(monitor, databaseConsumerSettings, producer.getDatabaseObject(), consumer.getTargetObject()); + databaseMappingContainer.getAttributeMappings(monitor); + databaseMappingContainer.setTarget(dataContainer); + consumer.setContainerMapping(databaseMappingContainer); + try { + producer.transferData(monitor, consumer, processorInstance, producerSettings, null); + if (monitor.isCanceled()) { + throw new DBWebException("Import is canceled"); + } + } catch (DBException e) { + throw new DBWebException("Import failed cause: " + e.getMessage()); + } + } + } + } diff --git a/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF index dcf8f1c8d1..16e97904cc 100644 --- a/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF @@ -3,11 +3,10 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - File System Bundle-SymbolicName: io.cloudbeaver.service.fs;singleton:=true -Bundle-Version: 1.0.0.qualifier -Bundle-Release-Date: 20230904 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 1.0.47.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy -Bundle-ClassPath: . -Require-Bundle: io.cloudbeaver.server, +Require-Bundle: io.cloudbeaver.server.ce, io.cloudbeaver.service.admin Automatic-Module-Name: io.cloudbeaver.service.fs diff --git a/server/bundles/io.cloudbeaver.service.fs/plugin.xml b/server/bundles/io.cloudbeaver.service.fs/plugin.xml index 45540f23a6..e3cbaf3f82 100644 --- a/server/bundles/io.cloudbeaver.service.fs/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.fs/plugin.xml @@ -8,5 +8,4 @@ class="io.cloudbeaver.service.fs.WebServiceBindingFS"> - diff --git a/server/bundles/io.cloudbeaver.service.fs/pom.xml b/server/bundles/io.cloudbeaver.service.fs/pom.xml index 67e34d889a..c2a4c0d10f 100644 --- a/server/bundles/io.cloudbeaver.service.fs/pom.xml +++ b/server/bundles/io.cloudbeaver.service.fs/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.fs - 1.0.0-SNAPSHOT + 1.0.47-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls b/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls index 558d088c43..cabd440d9a 100644 --- a/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls +++ b/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls @@ -1,34 +1,65 @@ type FSFile @since(version: "23.2.2") { + "Name of the file or folder" name: String! + "Length of the file in bytes" length: Int! + "Flag indicating if the file is a folder" folder: Boolean! + "Metadata of the file or folder" metaData: Object! + "Navigator tree node path" + nodePath: String! +} + +type FSFileSystem @since(version: "23.2.4") { + "File system ID" + id: ID! + "Navigator tree node path" + nodePath: String! + "External auth provider ID if file system requires authentication" + requiredAuth: String } extend type Query @since(version: "23.2.2") { - fsListFileSystems(projectId: ID!): [String!]! + "Returns available file systems for the specified project" + fsListFileSystems(projectId: ID!): [FSFileSystem!]! + + "Returns file system information for the specified project and node path" + fsFileSystem(projectId: ID!, nodePath: String!): FSFileSystem! @since(version: "23.2.4") - fsFile(projectId: ID!, fileURI: String!): FSFile! + "Returns file info for the specified node path" + fsFile(nodePath: String!): FSFile! - fsListFiles(projectId: ID!, folderURI: String!): [FSFile!]! + "Returns list of files and folders in the specified folder path" + fsListFiles(folderPath: String!): [FSFile!]! - # Reads file contents as string in UTF-8 - fsReadFileContentAsString(projectId: ID!, fileURI: String!): String! + "Reads file contents as string in UTF-8" + fsReadFileContentAsString(nodePath: String!): String! } extend type Mutation @since(version: "23.2.2") { - fsCreateFile(projectId: ID!, fileURI:String!): FSFile! + "Creates a new file in the specified parent path" + fsCreateFile(parentPath : String!, fileName : String!): FSFile! + + "Creates a new folder in the specified parent path" + fsCreateFolder(parentPath : String!, folderName: String!): FSFile! + + "Deletes file or folder by node path. Returns true if file was deleted, false if file not found" + fsDelete(nodePath : String!): Boolean! - fsCreateFolder(projectId: ID!, folderURI:String!): FSFile! + "Moves file or folder to the specified parent path. Returns updated file info" + fsMove(nodePath: String!, toParentNodePath: String!): FSFile! - fsDeleteFile(projectId: ID!, fileURI:String!): Boolean! + "Renames file or folder by node path. Returns updated file info" + fsRename(nodePath: String!, newName: String!): FSFile! - fsMoveFile(projectId: ID!, fromURI: String!, toURI: String!): FSFile! + "Copies file or folder to the specified parent path. Returns updated file info" + fsCopy(nodePath: String!, toParentNodePath: String!): FSFile! @since(version: "23.2.5") + "Writes string content to the file. If forceOverwrite is true then overwrites existing file, otherwise throws an error if file already exists" fsWriteFileStringContent( - projectId: ID!, - fileURI:String!, + nodePath:String!, data: String!, forceOverwrite: Boolean! - ): Boolean! + ): FSFile! } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java index fefd6d1a09..8f9c90b6e9 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -20,42 +20,46 @@ import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWService; import io.cloudbeaver.service.fs.model.FSFile; +import io.cloudbeaver.service.fs.model.FSFileSystem; import org.jkiss.code.NotNull; -import java.net.URI; - /** * Web service API */ public interface DBWServiceFS extends DBWService { @NotNull - String[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException; + FSFileSystem[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) + throws DBWebException; + @NotNull - FSFile getFile( + FSFileSystem getFileSystem( @NotNull WebSession webSession, @NotNull String projectId, - @NotNull URI fileURI + @NotNull String fileSystemId + ) throws DBWebException; + + @NotNull + FSFile getFile( + @NotNull WebSession webSession, + @NotNull String nodePath ) throws DBWebException; @NotNull FSFile[] getFiles( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI folderURI + @NotNull String nodePath ) throws DBWebException; @NotNull String readFileContent( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI fileURI + @NotNull String nodePath ) throws DBWebException; - boolean writeFileContent( + FSFile writeFileContent( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI fileURI, + @NotNull String nodePath, @NotNull String data, boolean forceOverwrite ) throws DBWebException; @@ -63,28 +67,38 @@ boolean writeFileContent( @NotNull FSFile createFile( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI fileURI + @NotNull String parentPath, + @NotNull String fileName ) throws DBWebException; FSFile moveFile( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI fromURI, - @NotNull URI toURI + @NotNull String nodePath, + @NotNull String parentNodePath + ) throws DBWebException; + + FSFile renameFile( + @NotNull WebSession webSession, + @NotNull String nodePath, + @NotNull String newName + ) throws DBWebException; + + FSFile copyFile( + @NotNull WebSession webSession, + @NotNull String nodePath, + @NotNull String parentNodePath ) throws DBWebException; @NotNull FSFile createFolder( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI folderURI + @NotNull String nodePath, + @NotNull String folderName ) throws DBWebException; boolean deleteFile( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI fileURI + @NotNull String nodePath ) throws DBWebException; } diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java index 4e0d92f056..2d8b5515fe 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,15 +17,20 @@ package io.cloudbeaver.service.fs; import io.cloudbeaver.DBWebException; +import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.service.DBWBindingContext; +import io.cloudbeaver.service.DBWServiceBindingServlet; +import io.cloudbeaver.service.DBWServletContext; import io.cloudbeaver.service.WebServiceBindingBase; import io.cloudbeaver.service.fs.impl.WebServiceFS; +import io.cloudbeaver.service.fs.model.WebFSServlet; +import org.jkiss.dbeaver.DBException; import org.jkiss.utils.CommonUtils; /** * Web service implementation */ -public class WebServiceBindingFS extends WebServiceBindingBase { +public class WebServiceBindingFS extends WebServiceBindingBase implements DBWServiceBindingServlet { private static final String SCHEMA_FILE_NAME = "schema/service.fs.graphqls"; @@ -38,54 +43,88 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { model.getQueryType() .dataFetcher("fsListFileSystems", env -> getService(env).getAvailableFileSystems(getWebSession(env), env.getArgument("projectId"))) + .dataFetcher("fsFileSystem", + env -> getService(env).getFileSystem( + getWebSession(env), + env.getArgument("projectId"), + env.getArgument("nodePath") + ) + ) .dataFetcher("fsFile", env -> getService(env).getFile(getWebSession(env), - env.getArgument("projectId"), - env.getArgument("fileURI")) + env.getArgument("nodePath") + ) ) .dataFetcher("fsListFiles", env -> getService(env).getFiles(getWebSession(env), - env.getArgument("projectId"), - env.getArgument("folderURI")) + env.getArgument("folderPath") + ) ) .dataFetcher("fsReadFileContentAsString", env -> getService(env).readFileContent(getWebSession(env), - env.getArgument("projectId"), - env.getArgument("fileURI")) + env.getArgument("nodePath") + ) ) ; model.getMutationType() .dataFetcher("fsCreateFile", env -> getService(env).createFile(getWebSession(env), - env.getArgument("projectId"), - env.getArgument("fileURI")) + env.getArgument("parentPath"), + env.getArgument("fileName") + ) ) .dataFetcher("fsCreateFolder", env -> getService(env).createFolder(getWebSession(env), - env.getArgument("projectId"), - env.getArgument("folderURI")) + env.getArgument("parentPath"), + env.getArgument("folderName") + ) ) - .dataFetcher("fsDeleteFile", + .dataFetcher("fsDelete", env -> getService(env).deleteFile(getWebSession(env), - env.getArgument("projectId"), - env.getArgument("fileURI")) + env.getArgument("nodePath") + ) ) - .dataFetcher("fsMoveFile", + .dataFetcher("fsMove", env -> getService(env).moveFile( getWebSession(env), - env.getArgument("projectId"), - env.getArgument("fromURI"), - env.getArgument("toURI")) + env.getArgument("nodePath"), + env.getArgument("toParentNodePath") + ) + ) + .dataFetcher("fsRename", + env -> getService(env).renameFile( + getWebSession(env), + env.getArgument("nodePath"), + env.getArgument("newName") + ) + ) + .dataFetcher("fsCopy", + env -> getService(env).copyFile( + getWebSession(env), + env.getArgument("nodePath"), + env.getArgument("toParentNodePath") + ) ) .dataFetcher("fsWriteFileStringContent", env -> getService(env).writeFileContent( getWebSession(env), - env.getArgument("projectId"), - env.getArgument("fileURI"), + env.getArgument("nodePath"), env.getArgument("data"), CommonUtils.toBoolean(env.getArgument("forceOverwrite")) ) ) ; } + + @Override + public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException { + if (!application.isMultiuser()) { + return; + } + servletContext.addServlet( + "fileSystems", + new WebFSServlet(application, getServiceImpl()), + application.getServicesURI() + "fs-data/*" + ); + } } diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java index 2acfa3e0c4..6dd3d2dcaa 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,32 +17,58 @@ package io.cloudbeaver.service.fs.impl; import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.fs.FSUtils; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.fs.DBWServiceFS; import io.cloudbeaver.service.fs.model.FSFile; +import io.cloudbeaver.service.fs.model.FSFileSystem; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystem; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.navigator.DBNProject; +import org.jkiss.dbeaver.model.navigator.fs.DBNFileSystem; +import org.jkiss.dbeaver.model.navigator.fs.DBNFileSystems; +import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase; +import org.jkiss.dbeaver.registry.fs.FileSystemProviderRegistry; -import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Web file system implementation */ public class WebServiceFS implements DBWServiceFS { + private static final Pattern FORBIDDEN_FILENAME_PATTERN = Pattern.compile("[%#:;№$]"); + @NotNull @Override - public String[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) + public FSFileSystem[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException { try { - return webSession.getFileSystemManager(projectId) - .getVirtualFileSystems() - .stream() - .map(DBFVirtualFileSystem::getType) - .toArray(String[]::new); + DBPProject project = webSession.getProjectById(projectId); + if (project == null) { + throw new DBException(MessageFormat.format("Project ''{0}'' is not found in session", projectId)); + } + DBNProject projectNode = webSession.getNavigatorModelOrThrow().getRoot().getProjectNode(project); + if (projectNode == null) { + throw new DBException(MessageFormat.format("Project ''{0}'' is not found in navigator model", projectId)); + } + DBNFileSystems dbnFileSystems = projectNode.getExtraNode(DBNFileSystems.class); + var fsRegistry = FileSystemProviderRegistry.getInstance(); + return Arrays.stream(dbnFileSystems.getChildren(webSession.getProgressMonitor())) + .map(fs -> new FSFileSystem( + FSUtils.makeUniqueFsId(fs.getFileSystem()), + fs.getNodeUri(), + fsRegistry.getProvider(fs.getFileSystem().getProviderId()).getRequiredAuth() + ) + ) + .toArray(FSFileSystem[]::new); } catch (Exception e) { throw new DBWebException("Failed to load file systems: " + e.getMessage(), e); } @@ -50,11 +76,34 @@ public String[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull @NotNull @Override - public FSFile getFile(@NotNull WebSession webSession, @NotNull String projectId, @NotNull URI fileUri) + public FSFileSystem getFileSystem( + @NotNull WebSession webSession, + @NotNull String projectId, + @NotNull String nodePath + ) throws DBWebException { + try { + var node = webSession.getNavigatorModelOrThrow().getNodeByPath(webSession.getProgressMonitor(), nodePath); + if (!(node instanceof DBNFileSystem fs)) { + throw new DBException(MessageFormat.format("Node ''{0}'' is not File System", nodePath)); + } + var fsRegistry = FileSystemProviderRegistry.getInstance(); + return new FSFileSystem( + FSUtils.makeUniqueFsId(fs.getFileSystem()), + fs.getNodeUri(), + fsRegistry.getProvider(fs.getFileSystem().getProviderId()).getRequiredAuth() + ); + } catch (Exception e) { + throw new DBWebException("Failed to get file system: " + e.getMessage(), e); + } + } + + @NotNull + @Override + public FSFile getFile(@NotNull WebSession webSession, @NotNull String nodePath) throws DBWebException { try { - Path filePath = webSession.getFileSystemManager(projectId).of(webSession.getProgressMonitor(), fileUri); - return new FSFile(filePath); + DBNPathBase node = FSUtils.getNodeByPath(webSession, nodePath); + return new FSFile(node); } catch (Exception e) { throw new DBWebException("Failed to found file: " + e.getMessage(), e); } @@ -62,14 +111,16 @@ public FSFile getFile(@NotNull WebSession webSession, @NotNull String projectId, @NotNull @Override - public FSFile[] getFiles(@NotNull WebSession webSession, @NotNull String projectId, @NotNull URI folderURI) + public FSFile[] getFiles(@NotNull WebSession webSession, @NotNull String parentPath) throws DBWebException { try { - Path folderPath = webSession.getFileSystemManager(projectId).of(webSession.getProgressMonitor(), folderURI); - try (var filesStream = Files.list(folderPath)) { - return filesStream.map(FSFile::new) - .toArray(FSFile[]::new); - } + DBNPathBase folderPath = FSUtils.getNodeByPath(webSession, parentPath); + var children = folderPath.getChildren(webSession.getProgressMonitor()); + return Arrays.stream(children) + .filter(c -> c instanceof DBNPathBase) + .map(c -> (DBNPathBase) c) + .map(FSFile::new) + .toArray(FSFile[]::new); } catch (Exception e) { throw new DBWebException("Failed to list files: " + e.getMessage(), e); } @@ -77,32 +128,33 @@ public FSFile[] getFiles(@NotNull WebSession webSession, @NotNull String project @NotNull @Override - public String readFileContent(@NotNull WebSession webSession, @NotNull String projectId, @NotNull URI fileUri) + public String readFileContent(@NotNull WebSession webSession, @NotNull String nodePath) throws DBWebException { try { - Path filePath = webSession.getFileSystemManager(projectId).of(webSession.getProgressMonitor(), fileUri); - return Files.readString(filePath); + Path filePath = FSUtils.getPathFromNode(webSession, nodePath); + var data = Files.readAllBytes(filePath); + return new String(data, StandardCharsets.UTF_8); } catch (Exception e) { throw new DBWebException("Failed to read file content: " + e.getMessage(), e); } } @Override - public boolean writeFileContent( + public FSFile writeFileContent( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI fileURI, + @NotNull String nodePath, @NotNull String data, boolean forceOverwrite ) throws DBWebException { try { - Path filePath = webSession.getFileSystemManager(projectId).of(webSession.getProgressMonitor(), fileURI); - if (!forceOverwrite && Files.exists(filePath)) { + DBNPathBase node = FSUtils.getNodeByPath(webSession, nodePath); + Path filePath = node.getPath(); + if (!forceOverwrite) { throw new DBException("Cannot overwrite exist file"); } Files.writeString(filePath, data); - return true; + return new FSFile(node); } catch (Exception e) { throw new DBWebException("Failed to write file content: " + e.getMessage(), e); } @@ -112,13 +164,18 @@ public boolean writeFileContent( @Override public FSFile createFile( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI fileUri + @NotNull String parentPath, + @NotNull String fileName ) throws DBWebException { try { - Path filePath = webSession.getFileSystemManager(projectId).of(webSession.getProgressMonitor(), fileUri); + DBNPathBase parentNode = FSUtils.getNodeByPath(webSession, parentPath); + if (!Files.isDirectory(parentNode.getPath())) { + throw new DBException(MessageFormat.format("Node ''{0}'' is not a directory", parentPath)); + } + Path filePath = parentNode.getPath().resolve(fileName); Files.createFile(filePath); - return new FSFile(filePath); + parentNode.addChildResource(filePath); + return new FSFile(parentNode.getChild(filePath)); } catch (Exception e) { throw new DBWebException("Failed to create file: " + e.getMessage(), e); } @@ -127,31 +184,82 @@ public FSFile createFile( @Override public FSFile moveFile( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI fromURI, - @NotNull URI toURI + @NotNull String oldNodePath, + @NotNull String parentNodePath + ) throws DBWebException { + try { + DBNPathBase oldNode = FSUtils.getNodeByPath(webSession, oldNodePath); + DBNPathBase oldParentNode = (DBNPathBase) oldNode.getParentNode(); + String fileName = oldNode.getName(); + DBNPathBase parentNode = FSUtils.getNodeByPath(webSession, parentNodePath); + Path parentPath = parentNode.getPath(); + if (!Files.isDirectory(parentPath)) { + throw new DBException(MessageFormat.format("Node ''{0}'' is not a directory", parentPath)); + } + Path to = Files.move(oldNode.getPath(), parentPath.resolve(fileName)); + // apply changes in navigator node + oldParentNode.removeChildResource(oldNode.getPath()); + parentNode.addChildResource(to); + return new FSFile(parentNode.getChild(to)); + } catch (Exception e) { + throw new DBWebException("Failed to move file: " + e.getMessage(), e); + } + } + + @Override + public FSFile renameFile( + @NotNull WebSession webSession, + @NotNull String nodePath, + @NotNull String newName ) throws DBWebException { + validateFilename(newName); try { - Path from = webSession.getFileSystemManager(projectId).of(webSession.getProgressMonitor(), fromURI); - Path to = webSession.getFileSystemManager(projectId).of(webSession.getProgressMonitor(), toURI); - Files.move(from, to); - return new FSFile(to); + DBNPathBase node = FSUtils.getNodeByPath(webSession, nodePath); + node.rename(webSession.getProgressMonitor(), newName); + return new FSFile(node); } catch (Exception e) { throw new DBWebException("Failed to move file: " + e.getMessage(), e); } } + @Override + public FSFile copyFile( + @NotNull WebSession webSession, + @NotNull String oldNodePath, + @NotNull String parentNodePath + ) throws DBWebException { + try { + DBNPathBase oldNode = FSUtils.getNodeByPath(webSession, oldNodePath); + String fileName = oldNode.getName(); + DBNPathBase parentNode = FSUtils.getNodeByPath(webSession, parentNodePath); + Path parentPath = parentNode.getPath(); + if (!Files.isDirectory(parentPath)) { + throw new DBException(MessageFormat.format("Node ''{0}'' is not a directory", parentPath)); + } + Path to = Files.copy(oldNode.getPath(), parentPath.resolve(fileName)); + parentNode.addChildResource(to); + return new FSFile(parentNode.getChild(to)); + } catch (Exception e) { + throw new DBWebException("Failed to copy file: " + e.getMessage(), e); + } + } + @NotNull @Override public FSFile createFolder( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI folderURI + @NotNull String parentPath, + @NotNull String folderName ) throws DBWebException { try { - Path folderPath = webSession.getFileSystemManager(projectId).of(webSession.getProgressMonitor(), folderURI); + DBNPathBase parentNode = FSUtils.getNodeByPath(webSession, parentPath); + if (!Files.isDirectory(parentNode.getPath())) { + throw new DBException(MessageFormat.format("Node ''{0}'' is not a directory", parentPath)); + } + Path folderPath = parentNode.getPath().resolve(folderName); Files.createDirectory(folderPath); - return new FSFile(folderPath); + parentNode.addChildResource(folderPath); + return new FSFile(parentNode.getChild(folderPath)); } catch (Exception e) { throw new DBWebException("Failed to create folder: " + e.getMessage(), e); } @@ -160,15 +268,25 @@ public FSFile createFolder( @Override public boolean deleteFile( @NotNull WebSession webSession, - @NotNull String projectId, - @NotNull URI fileUri + @NotNull String nodePath ) throws DBWebException { try { - Path filePath = webSession.getFileSystemManager(projectId).of(webSession.getProgressMonitor(), fileUri); - Files.delete(filePath); + DBNPathBase node = FSUtils.getNodeByPath(webSession, nodePath); + Path path = node.getPath(); + Files.delete(path); + DBNPathBase parentNode = (DBNPathBase) node.getParentNode(); + parentNode.removeChildResource(path); return true; } catch (Exception e) { throw new DBWebException("Failed to create folder: " + e.getMessage(), e); } } + + private void validateFilename(@NotNull String filename) throws DBWebException { + Matcher matcher = FORBIDDEN_FILENAME_PATTERN.matcher(filename); + + if (matcher.find()) { + throw new DBWebException(String.format("File %s contains forbidden symbols", filename)); + } + } } diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/FSFile.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/FSFile.java index 757a956f52..d7cf1a1813 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/FSFile.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/FSFile.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,44 +16,45 @@ */ package io.cloudbeaver.service.fs.model; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.meta.Property; -import org.jkiss.utils.ArrayUtils; +import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.util.Map; public class FSFile { - private final Path path; + @NotNull + private final DBNPathBase node; - public FSFile(Path path) { - this.path = path; + public FSFile(@NotNull DBNPathBase node) { + this.node = node; } @Property public String getName() { - String[] pathParts = path.getFileName().toString().split(path.getFileSystem().getSeparator()); - if (ArrayUtils.isEmpty(pathParts)) { - return ""; - } - return pathParts[pathParts.length - 1]; + return node.getNodeDisplayName(); } @Property - public long getLength() throws IOException { - return Files.size(path); + return Files.size(node.getPath()); } @Property - public boolean isFolder() { - return Files.isDirectory(path); + return Files.isDirectory(node.getPath()); } @Property public Map getMetaData() { return Map.of(); } + + @Property + //TODO: node URI after finish migration to the new node path format + public String getNodePath() { + return node.getNodeItemPath(); + } } diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/FSFileSystem.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/FSFileSystem.java new file mode 100644 index 0000000000..ac1ca112bd --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/FSFileSystem.java @@ -0,0 +1,23 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.fs.model; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; + +public record FSFileSystem(@NotNull String id, @NotNull String nodePath, @Nullable String requiredAuth) { +} diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java new file mode 100644 index 0000000000..ce4e978424 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java @@ -0,0 +1,124 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.fs.model; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.fs.FSUtils; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.service.WebServiceServletBase; +import io.cloudbeaver.service.fs.DBWServiceFS; +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.annotation.MultipartConfig; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Part; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +@MultipartConfig() +public class WebFSServlet extends WebServiceServletBase { + + private static final Log log = Log.getLog(WebFSServlet.class); + private static final String PARAM_PROJECT_ID = "projectId"; + private final DBWServiceFS fs; + + public WebFSServlet(CBApplication application, DBWServiceFS fs) { + super(application); + this.fs = fs; + } + + @Override + protected void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException { + if (!session.isAuthorizedInSecurityManager()) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Anonymous access restricted."); + return; + } + if (request.getMethod().equals("POST")) { + doPost(session, request, response); + } else { + doGet(session, request, response); + } + + } + + private void doGet(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException { + Path path = FSUtils.getPathFromNode(session, request.getParameter("nodePath")); + session.addInfoMessage("Download data ..."); + response.setHeader("Content-Type", "application/octet-stream"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + path.getFileName() + "\""); + response.setHeader("Content-Length", String.valueOf(Files.size(path))); + + try (InputStream is = Files.newInputStream(path)) { + IOUtils.copyStream(is, response.getOutputStream()); + } + } + + private void doPost(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException { + // we need to set this attribute to get parts + request.setAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT, new MultipartConfigElement("")); + Map variables = getVariables(request); + String parentNodePath = JSONUtils.getString(variables, "toParentNodePath"); + if (CommonUtils.isEmpty(parentNodePath)) { + throw new DBException("Parent node path parameter is not found"); + } + DBNPathBase node = FSUtils.getNodeByPath(session, parentNodePath); + Path path = node.getPath(); + try { + for (Part part : request.getParts()) { + String fileName = part.getSubmittedFileName(); + if (CommonUtils.isEmpty(fileName)) { + continue; + } + try (InputStream is = part.getInputStream()) { + Files.copy(is, path.resolve(fileName)); + node.addChildResource(path.resolve(fileName)); + } + } + } catch (Exception e) { + throw new DBWebException("File Upload Failed: Unable to Save File to the File System", + CommonUtils.getRootCause(e)); + } + } + + @Override + protected Map getVariables(HttpServletRequest request) { + Map variables = super.getVariables(request); + try { + for (Part part : request.getParts()) { + if (part.getSubmittedFileName()!= null && !part.getSubmittedFileName().isEmpty()) { + variables.put("fileName", part.getSubmittedFileName()); + break; + } + } + } catch (Exception e) { + log.debug("Failed to get fileName from request for logging", e); + } + return variables; + } +} diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.ldap.auth/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..dd687bf6d0 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/META-INF/MANIFEST.MF @@ -0,0 +1,16 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Cloudbeaver Web Service - LDAP +Bundle-Vendor: DBeaver Corp +Bundle-SymbolicName: io.cloudbeaver.service.ldap.auth;singleton:=true +Bundle-Version: 1.0.12.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 +Bundle-ActivationPolicy: lazy +Require-Bundle: org.jkiss.dbeaver.model;visibility:=reexport, + org.jkiss.dbeaver.registry;visibility:=reexport, + io.cloudbeaver.model +Bundle-Localization: OSGI-INF/l10n/bundle +Export-Package: io.cloudbeaver.service.ldap.auth, + io.cloudbeaver.service.ldap.auth.ssl +Automatic-Module-Name: io.cloudbeaver.service.ldap.auth diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle.properties b/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle.properties new file mode 100644 index 0000000000..6c79f27c9d --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle.properties @@ -0,0 +1,25 @@ +prop.auth.model.ldap.ldap-host = Host +prop.auth.model.ldap.ldap-host.description = LDAP server host +prop.auth.model.ldap.ldap-port = Port +prop.auth.model.ldap.ldap-port.description = LDAP server port, default is 389 +prop.auth.model.ldap.ldap-identifier-attr = User identifier attribute +prop.auth.model.ldap.ldap-identifier-attr.description = LDAP attribute used as a user ID. Will be automatically added to the beginning of the 'User DN' value during authorization if not explicitly specified +prop.auth.model.ldap.ldap-dn = Base Distinguished Name +prop.auth.model.ldap.ldap-dn.description = Base Distinguished Name applicable for all users, example: dc=myOrg,dc=com. Will be automatically added to the end of the 'User DN' value during authorization if not explicitly specified. Leave blank if you want to use the root DN. +prop.auth.model.ldap.ldap-bind-user = Bind User DN +prop.auth.model.ldap.ldap-bind-user.description = DN of user, who has permissions to search for users to check access to the application with the specified filter. +prop.auth.model.ldap.ldap-bind-user-pwd = Bind User Password +prop.auth.model.ldap.ldap-bind-user-pwd.description = Bind user password. +prop.auth.model.ldap.ldap-filter = User Filter +prop.auth.model.ldap.ldap-filter.description = Filter that will be used to verify users access to the application. To use the filter, the bind user configuration is mandatory. +prop.auth.model.ldap.user-dn = User DN +prop.auth.model.ldap.user-dn.description = LDAP user name +prop.auth.model.ldap.password = User password +prop.auth.model.ldap.password.description = LDAP user password +prop.auth.model.ldap.ldap-login = User login parameter +prop.auth.model.ldap.ldap-login.description = LDAP attribute to be used as the user login. The attribute must be unique. Configuring the bind user is mandatory to use this parameter. +prop.auth.model.ldap.ldap-enable-ssl = Enable SSL +prop.auth.model.ldap.ldap-enable-ssl.description = Use a secure SSL connection to the LDAP server +prop.auth.model.ldap.ldap-ssl-cert = Certificate (PEM format) +prop.auth.model.ldap.ldap-ssl-cert.description = PEM-encoded certificate used to validate the LDAP server when SSL is enabled + diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle_ru.properties b/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle_ru.properties new file mode 100644 index 0000000000..274a04ec19 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle_ru.properties @@ -0,0 +1,24 @@ +prop.auth.model.ldap.ldap-host = Хост +prop.auth.model.ldap.ldap-host.description = Хост сервера LDAP +prop.auth.model.ldap.ldap-port = Порт +prop.auth.model.ldap.ldap-port.description = Порт LDAP-сервера, по умолчанию 389 +prop.auth.model.ldap.ldap-identifier-attr = Атрибут идентификатора пользователя +prop.auth.model.ldap.ldap-identifier-attr.description = Атрибут LDAP, используемый в качестве идентификатора пользователя. Будет автоматически добавлен в начало значения 'User DN' при авторизации, если не указан явно +prop.auth.model.ldap.ldap-dn = Базовое отличительное имя +prop.auth.model.ldap.ldap-dn.description = Базовое отличительное имя, применимое ко всем пользователям, например: dc=myOrg,dc=com. Будет автоматически добавлено в конец значения 'User DN' при авторизации, если не указано явно. Оставьте пустым если хотите использовать root DN. +prop.auth.model.ldap.ldap-bind-user = Привязка DN пользователя +prop.auth.model.ldap.ldap-bind-user.description = DN пользователя, который имеет права на поиск пользователей для проверки доступа к приложению с указанным фильтром. +prop.auth.model.ldap.ldap-bind-user-pwd = Связать пароль пользователя +prop.auth.model.ldap.ldap-bind-user-pwd.description = Привязка пароля пользователя. +prop.auth.model.ldap.ldap-filter = Фильтр пользователя +prop.auth.model.ldap.ldap-filter.description = Фильтр, который будет использоваться для проверки доступа пользователей к приложению. Для использования фильтра обязательна настройка bind user. +prop.auth.model.ldap.user-dn = Имя пользователя DN +prop.auth.model.ldap.user-dn.description = LDAP имя пользователя +prop.auth.model.ldap.password = Пароль пользователя +prop.auth.model.ldap.password.description = LDAP пароль пользователя +prop.auth.model.ldap.ldap-login = Параметр логина +prop.auth.model.ldap.ldap-login.description = Атрибут LDAP, который будет использоваться в качестве логина пользователя. Атрибут должен быть уникальным. Настройка привязки пользователя обязательна для использования этого параметра. +prop.auth.model.ldap.ldap-enable-ssl = Включить SSL +prop.auth.model.ldap.ldap-enable-ssl.description = Использовать защищённое SSL-соединение с сервером LDAP +prop.auth.model.ldap.ldap-ssl-cert = Сертификат (в формате PEM) +prop.auth.model.ldap.ldap-ssl-cert.description = PEM-сертификат, используемый для проверки подлинности LDAP-сервера при включённом SSL \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/build.properties b/server/bundles/io.cloudbeaver.service.ldap.auth/build.properties new file mode 100644 index 0000000000..3c833df018 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/build.properties @@ -0,0 +1,6 @@ +source.. = src/ +output.. = target/classes/ +bin.includes = .,\ + META-INF/,\ + OSGI-INF/,\ + plugin.xml diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/icons/ldap.svg b/server/bundles/io.cloudbeaver.service.ldap.auth/icons/ldap.svg new file mode 100644 index 0000000000..7f0c9e4164 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/icons/ldap.svg @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml b/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml new file mode 100644 index 0000000000..b0f570e6f0 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/pom.xml b/server/bundles/io.cloudbeaver.service.ldap.auth/pom.xml new file mode 100644 index 0000000000..984693c0bf --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + io.cloudbeaver + bundles + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.service.ldap.auth + 1.0.12-SNAPSHOT + eclipse-plugin + + diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java new file mode 100644 index 0000000000..7cff10129b --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java @@ -0,0 +1,582 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.ldap.auth; + +import io.cloudbeaver.DBWUserIdentity; +import io.cloudbeaver.auth.SMAuthProviderAssigner; +import io.cloudbeaver.auth.SMAuthProviderExternal; +import io.cloudbeaver.auth.SMAutoAssign; +import io.cloudbeaver.auth.SMBruteForceProtected; +import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.model.user.WebUser; +import io.cloudbeaver.service.ldap.auth.ssl.LdapSslSetting; +import io.cloudbeaver.service.ldap.auth.ssl.LdapSslSocketFactory; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPObject; +import org.jkiss.dbeaver.model.auth.SMSession; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; +import org.jkiss.dbeaver.model.security.SMController; +import org.jkiss.utils.CommonUtils; + +import java.io.ByteArrayInputStream; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.*; +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.*; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +public class LdapAuthProvider implements SMAuthProviderExternal, SMBruteForceProtected, SMAuthProviderAssigner { + private static final Log log = Log.getLog(LdapAuthProvider.class); + public static final String LDAP_AUTH_PROVIDER_ID = "ldap"; + + public LdapAuthProvider() { + } + + @NotNull + @Override + public Map authExternalUser( + @NotNull DBRProgressMonitor monitor, + @Nullable SMAuthProviderCustomConfiguration providerConfig, + @NotNull Map authParameters + ) throws DBException { + if (providerConfig == null) { + throw new DBException("LDAP provider config is null"); + } + String userName = JSONUtils.getString(authParameters, LdapConstants.CRED_USER_DN); + if (CommonUtils.isEmpty(userName)) { + throw new DBException("LDAP user dn is empty"); + } + String password = JSONUtils.getString(authParameters, LdapConstants.CRED_PASSWORD); + if (CommonUtils.isEmpty(password)) { + throw new DBException("LDAP password is empty"); + } + + LdapSettings ldapSettings = new LdapSettings(providerConfig); + Map environment = creteAuthEnvironment(ldapSettings); + + Map userData = new HashMap<>(); + if (!isFullDN(userName) && CommonUtils.isNotEmpty(ldapSettings.getLoginAttribute())) { + userData = validateAndLoginUserAccessByUsername(userName, password, ldapSettings); + } + if (CommonUtils.isEmpty(userData)) { + String fullUserDN = buildFullUserDN(userName, ldapSettings); + validateUserAccess(fullUserDN, ldapSettings); + userData = authenticateLdap(fullUserDN, password, ldapSettings, null, environment); + } + return userData; + } + + @NotNull + @Override + public SMAutoAssign detectAutoAssignments( + @NotNull DBRProgressMonitor monitor, + @NotNull SMAuthProviderCustomConfiguration providerConfig, + @NotNull Map authParameters + ) throws DBException { + List autoAssignmentTeamIds = detectAutoAssignmentTeam(providerConfig, authParameters); + SMAutoAssign smAutoAssign = new SMAutoAssign(); + autoAssignmentTeamIds.forEach(smAutoAssign::addExternalTeamId); + return smAutoAssign; + } + + @Override + public void postAuthentication() { + LdapSslSocketFactory.removeContextFactory(); + } + + @Nullable + @Override + public String getExternalTeamIdMetadataFieldName() { + return LdapConstants.LDAP_META_GROUP_NAME; + } + + /** + * Find user and validate in ldap by uniq parameter from identityProviders + * + */ + private Map validateAndLoginUserAccessByUsername( + @NotNull String login, + @NotNull String password, + @NotNull LdapSettings ldapSettings + ) throws DBException { + if ( + CommonUtils.isEmpty(ldapSettings.getBindUserDN()) + || CommonUtils.isEmpty(ldapSettings.getBindUserPassword()) + ) { + return null; + } + Map serviceUserContext = creteAuthEnvironment(ldapSettings); + serviceUserContext.put(Context.SECURITY_PRINCIPAL, ldapSettings.getBindUserDN()); + serviceUserContext.put(Context.SECURITY_CREDENTIALS, ldapSettings.getBindUserPassword()); + DirContext serviceContext; + + try { + serviceContext = initConnection(serviceUserContext); + String userDN = findUserDN(serviceContext, ldapSettings, login); + if (userDN == null) { + return null; + } + return authenticateLdap(userDN, password, ldapSettings, login, creteAuthEnvironment(ldapSettings)); + } catch (Exception e) { + throw new DBException("LDAP authentication failed: " + e.getMessage(), e); + } + } + + /** + * Find user and validate in ldap by fullUserDN + */ + private void validateUserAccess( + @NotNull String fullUserDN, + @NotNull LdapSettings ldapSettings + ) throws DBException { + if ( + CommonUtils.isEmpty(ldapSettings.getFilter()) + || CommonUtils.isEmpty(ldapSettings.getBindUserDN()) + || CommonUtils.isEmpty(ldapSettings.getBindUserPassword()) + ) { + return; + } + + var environment = creteAuthEnvironment(ldapSettings); + environment.put(Context.SECURITY_PRINCIPAL, ldapSettings.getBindUserDN()); + environment.put(Context.SECURITY_CREDENTIALS, ldapSettings.getBindUserPassword()); + DirContext bindUserContext; + try { + bindUserContext = initConnection(environment); + SearchControls searchControls = createSearchControls(); + var searchResult = bindUserContext.search(fullUserDN, ldapSettings.getFilter(), searchControls); + if (!searchResult.hasMore()) { + throw new DBException("Access denied"); + } + } catch (DBException e) { + throw e; + } catch (Exception e) { + throw new DBException("LDAP user access validation by filter failed: " + e.getMessage(), e); + } + } + + protected String getAttributeValue(Attributes attributes, String attributeName) throws NamingException { + Attribute attribute = attributes.get(attributeName); + return attribute != null ? attribute.get().toString() : null; + } + + @NotNull + protected String getAttributeValueSafe(@NotNull Attributes attributes, @NotNull String attrName) { + try { + Attribute attr = attributes.get(attrName.toLowerCase()); + return attr != null ? (String) attr.get() : ""; + } catch (Exception e) { + log.debug("Can't extract '" + attrName + "' from ldap attributes"); + return ""; + } + } + + @NotNull + public Map creteAuthEnvironment(LdapSettings ldapSettings) throws DBException { + Map environment = new HashMap<>(); + environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + + environment.put(Context.PROVIDER_URL, ldapSettings.getLdapProviderUrl()); + environment.put(Context.SECURITY_AUTHENTICATION, "simple"); + + try { + configureSsl(ldapSettings, environment); + } catch (Exception e) { + log.error("Can't establish ssl connection", e); + throw new DBException("Can't establish ssl connection", e); + } + + return environment; + } + + private void configureSsl(LdapSettings ldapSettings, Map environment) throws Exception { + LdapSslSetting ldapSslSetting = ldapSettings.getLdapSslSetting(); + + if (!ldapSslSetting.isEnable() || CommonUtils.isEmpty(ldapSslSetting.getSslCert())) { + return; + } + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + byte[] decoded = Base64.getDecoder().decode(ldapSslSetting.getSslCert()); + ByteArrayInputStream certStream = new ByteArrayInputStream(decoded); + Certificate cert = cf.generateCertificate(certStream); + + KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType()); + ts.load(null, null); + ts.setCertificateEntry("trusted-root", cert); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ts); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), new SecureRandom()); + + LdapSslSocketFactory.setContextFactory(sslContext); + environment.put("java.naming.ldap.factory.socket", LdapSslSocketFactory.class.getName()); + } + + protected String findUserDN(DirContext serviceContext, LdapSettings ldapSettings, String userIdentifier) throws DBException { + + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(new String[]{"distinguishedName"}); + NamingEnumeration results = findByFilter( + serviceContext, + ldapSettings, + buildSearchFilter(ldapSettings, userIdentifier), + searchControls + ); + + try { + if (results.hasMore()) { + return results.next().getNameInNamespace(); + } + } catch (NamingException e) { + throw new DBException("Error finding user DN: " + e.getMessage(), e); + } + return null; + } + + public NamingEnumeration findByFilter( + @NotNull DirContext serviceContext, + @NotNull LdapSettings ldapSettings, + @NotNull String searchFilter, + @NotNull SearchControls searchControls + ) throws DBException { + try { + String baseDN = getBaseDN(serviceContext, ldapSettings); + return serviceContext.search(baseDN, searchFilter, searchControls); + } catch (Exception e) { + throw new DBException("Error finding user DN: " + e.getMessage(), e); + } + } + + private String getBaseDN(DirContext serviceContext, LdapSettings ldapSettings) throws DBException { + if (CommonUtils.isEmpty(ldapSettings.getBaseDN())) { + return getRootDN(serviceContext); + } + return ldapSettings.getBaseDN(); + } + + private String buildSearchFilter(LdapSettings ldapSettings, String userIdentifier) { + String userFilter = String.format("(%s=%s)", ldapSettings.getLoginAttribute(), userIdentifier); + if (CommonUtils.isNotEmpty(ldapSettings.getFilter())) { + return String.format("(&%s%s)", userFilter, ldapSettings.getFilter()); + } + return userFilter; + } + + private String getRootDN(DirContext adminContext) throws DBException { + try { + Attributes attributes = adminContext.getAttributes("", new String[]{"namingContexts"}); + Attribute namingContexts = attributes.get("namingContexts"); + if (namingContexts != null && namingContexts.size() > 0) { + return (String) namingContexts.get(0); + } + throw new DBException("Root DN not found in namingContexts"); + } catch (Exception e) { + throw new DBException("Error retrieving root DN: " + e.getMessage(), e); + } + } + + @NotNull + private String findUserNameFromDN(@NotNull String fullUserDN, @NotNull LdapSettings ldapSettings) + throws DBException { + String userId = null; + for (String dn : fullUserDN.split(",")) { + if (dn.startsWith(ldapSettings.getUserIdentifierAttr() + "=")) { + userId = dn.split("=")[1]; + break; + } + } + if (userId == null) { + throw new DBException("Failed to determine userId from user DN: " + fullUserDN); + } + return userId; + } + + @NotNull + @Override + public DBWUserIdentity getUserIdentity( + @NotNull DBRProgressMonitor monitor, + @Nullable SMAuthProviderCustomConfiguration customConfiguration, + @NotNull Map authParameters + ) throws DBException { + String userName = JSONUtils.getString(authParameters, LocalAuthProviderConstants.CRED_USER); + if (CommonUtils.isEmpty(userName)) { + throw new DBException("LDAP user name is empty"); + } + String displayName = JSONUtils.getString(authParameters, LocalAuthProviderConstants.CRED_DISPLAY_NAME); + if (CommonUtils.isEmpty(displayName)) { + displayName = userName; + } + return new DBWUserIdentity(userName, displayName); + } + + @Nullable + @Override + public DBPObject getUserDetails( + @NotNull DBRProgressMonitor monitor, + @NotNull WebSession webSession, + @NotNull SMSession session, + @NotNull WebUser user, + boolean selfIdentity + ) throws DBException { + return null; + } + + @NotNull + @Override + public String validateLocalAuth( + @NotNull DBRProgressMonitor monitor, + @NotNull SMController securityController, + @NotNull SMAuthProviderCustomConfiguration providerConfig, + @NotNull Map userCredentials, + @Nullable String activeUserId + ) throws DBException { + String userId = JSONUtils.getString(userCredentials, LdapConstants.CRED_USERNAME); + String oldUsername = JSONUtils.getString(userCredentials, LdapConstants.CRED_USER_DN); + if (CommonUtils.isNotEmpty(oldUsername)) { + oldUsername = findUserNameFromDN(oldUsername, new LdapSettings(providerConfig)); + Map oldUserLDAP = securityController.getUserCredentials(oldUsername, LDAP_AUTH_PROVIDER_ID); + userCredentials.putAll(oldUserLDAP); + if (userCredentials.get("user").equals(oldUsername)) { + userId = oldUsername; + } + } + if (CommonUtils.isEmpty(userId)) { + throw new DBException("LDAP user id not found"); + } + return activeUserId == null ? userId : activeUserId; + } + + @Override + public SMSession openSession( + @NotNull DBRProgressMonitor monitor, + @NotNull SMSession mainSession, + @Nullable SMAuthProviderCustomConfiguration customConfiguration, + @NotNull Map userCredentials + ) throws DBException { + return new LdapSession(mainSession, mainSession.getSessionSpace(), userCredentials); + } + + @Override + public void closeSession(@NotNull SMSession mainSession, SMSession session) throws DBException { + + } + + @Override + public void refreshSession( + @NotNull DBRProgressMonitor monitor, + @NotNull SMSession mainSession, + SMSession session + ) throws DBException { + + } + + @Override + public Object getInputUsername(@NotNull Map cred) { + return cred.get(LdapConstants.CRED_USER_DN); + } + + private boolean isFullDN(String userName) { + return userName.contains(",") && userName.contains("="); + } + + private String buildFullUserDN(String userName, LdapSettings ldapSettings) { + String fullUserDN = userName; + + if (!fullUserDN.startsWith(ldapSettings.getUserIdentifierAttr())) { + fullUserDN = String.join("=", ldapSettings.getUserIdentifierAttr(), userName); + } + if (CommonUtils.isNotEmpty(ldapSettings.getBaseDN()) && !fullUserDN.endsWith(ldapSettings.getBaseDN())) { + fullUserDN = String.join(",", fullUserDN, ldapSettings.getBaseDN()); + } + + return fullUserDN; + } + + private SearchControls createSearchControls() { + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setTimeLimit(30_000); + searchControls.setReturningAttributes(new String[]{"*", "+"}); + return searchControls; + } + + private Map authenticateLdap( + @NotNull String userDN, + @NotNull String password, + @NotNull LdapSettings ldapSettings, + @Nullable String login, + @NotNull Map environment + ) throws DBException { + Map userData = new HashMap<>(); + environment.put(Context.SECURITY_PRINCIPAL, userDN); + environment.put(Context.SECURITY_CREDENTIALS, password); + DirContext userContext = null; + try { + userContext = initConnection(environment); + SearchControls searchControls = createSearchControls(); + String userId = ""; + var searchResult = userContext.search(userDN, "objectClass=*", searchControls); + if (searchResult.hasMore()) { + SearchResult result = searchResult.next(); + Attributes attributes = result.getAttributes(); + userId = getAttributeValue(attributes, "objectGUID"); + if (userId == null) { + userId = getAttributeValue(attributes, "entryUUID"); + } + userData.put( + LdapConstants.LDAP_META_GROUP_NAME, + getAttributeValueSafe( + attributes, + ldapSettings.getProviderConfiguration().getParameter(LdapConstants.LDAP_META_GROUP_NAME) + ) + ); + doCustomModifyUserDataAfterAuthentication(ldapSettings, attributes, userData); + } + userData.putIfAbsent(LdapConstants.CRED_USERNAME, CommonUtils.isNotEmpty(login) ? login : userId); + userData.put(LdapConstants.CRED_USER_DN, userDN); + userData.put(LdapConstants.CRED_PASSWORD, password); + userData.put(LdapConstants.CRED_DISPLAY_NAME, CommonUtils.isNotEmpty(login) ? login : findUserNameFromDN(userDN, ldapSettings)); + userData.put(LdapConstants.CRED_SESSION_ID, UUID.randomUUID()); + + return userData; + } catch (Exception e) { + throw new DBException("LDAP authentication failed: " + e.getMessage(), e); + } finally { + if (userContext != null) { + try { + userContext.close(); + } catch (NamingException e) { + log.warn("Error closing LDAP user context", e); + } + } + } + } + + protected void doCustomModifyUserDataAfterAuthentication(LdapSettings ldapSettings, Attributes attributes, Map userData) { + } + + @NotNull + protected List detectAutoAssignmentTeam( + @NotNull SMAuthProviderCustomConfiguration providerConfig, + @NotNull Map authParameters + ) throws DBException { + String userName = JSONUtils.getString(authParameters, LdapConstants.CRED_USERNAME); + if (CommonUtils.isEmpty(userName)) { + throw new DBException("LDAP user name is empty"); + } + + LdapSettings ldapSettings = new LdapSettings(providerConfig); + String fullDN = JSONUtils.getString(authParameters, LdapConstants.CRED_USER_DN); + String userDN; + if (!CommonUtils.isEmpty(fullDN)) { + userDN = fullDN; + } else { + userDN = getUserDN(ldapSettings, JSONUtils.getString(authParameters, LdapConstants.CRED_DISPLAY_NAME)); + } + if (userDN == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + result.add(userDN); + result.addAll(getGroupForMember(userDN, ldapSettings, authParameters)); + return result; + } + + private String getUserDN(LdapSettings ldapSettings, String displayName) { + DirContext context; + try { + context = initConnection(creteAuthEnvironment(ldapSettings)); + return findUserDN(context, ldapSettings, displayName); + } catch (Exception e) { + log.error("User not found", e); + return null; + } + } + + @NotNull + private List getGroupForMember(String fullDN, LdapSettings ldapSettings, Map authParameters) { + DirContext context = null; + NamingEnumeration searchResults = null; + List result = new ArrayList<>(); + try { + Map environment = creteAuthEnvironment(ldapSettings); + if (CommonUtils.isEmpty(ldapSettings.getBindUserDN())) { + environment.put(Context.SECURITY_PRINCIPAL, String.valueOf(authParameters.get(LdapConstants.CRED_USER_DN))); + environment.put(Context.SECURITY_CREDENTIALS, String.valueOf(authParameters.get(LdapConstants.CRED_PASSWORD))); + } else { + environment.put(Context.SECURITY_PRINCIPAL, ldapSettings.getBindUserDN()); + environment.put(Context.SECURITY_CREDENTIALS, ldapSettings.getBindUserPassword()); + } + //it's a hack. Otherwise password will be written to database + authParameters.remove(LdapConstants.CRED_PASSWORD); + + context = initConnection(environment); + + String searchFilter = "(member=" + fullDN + ")"; + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + + searchResults = context.search(ldapSettings.getBaseDN(), searchFilter, searchControls); + while (searchResults.hasMore()) { + result.add(searchResults.next().getName()); + } + } catch (Exception e) { + log.error("Group not found", e); + } finally { + try { + if (context != null) { + context.close(); + } + if (searchResults != null) { + searchResults.close(); + } + } catch (Exception e) { + log.error("Close resource of ldap group search failed", e); + } + } + return result; + } + + public DirContext initConnection(Map environment) throws DBException { + //this hack is needed for correct LDAPS working. JNDI uses ContextClassLoader instead of OSGI loader + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + try { + return new InitialDirContext(new Hashtable<>(environment)); + } catch (Exception e) { + throw new DBException("Can't establish LDAP connection", e); + } finally { + Thread.currentThread().setContextClassLoader(previous); + } + } +} diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java new file mode 100644 index 0000000000..86fe8885f4 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java @@ -0,0 +1,37 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.ldap.auth; + +public interface LdapConstants { + String PARAM_HOST = "ldap-host"; + String PARAM_PORT = "ldap-port"; + String PARAM_DN = "ldap-dn"; + String PARAM_BIND_USER = "ldap-bind-user"; + String PARAM_BIND_USER_PASSWORD = "ldap-bind-user-pwd"; + String PARAM_FILTER = "ldap-filter"; + String PARAM_USER_IDENTIFIER_ATTR = "ldap-identifier-attr"; + String PARAM_LOGIN = "ldap-login"; + String PARAM_SSL_ENABLE = "ldap-enable-ssl"; + String PARAM_SSL_CERT = "ldap-ssl-cert"; + + String CRED_USERNAME = "user"; + String CRED_DISPLAY_NAME = "displayName"; + String CRED_USER_DN = "user-dn"; + String CRED_PASSWORD = "password"; + String CRED_SESSION_ID = "session-id"; + String LDAP_META_GROUP_NAME = "ldap.group-name"; +} diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSession.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSession.java new file mode 100644 index 0000000000..c4326af395 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSession.java @@ -0,0 +1,41 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.ldap.auth; + +import io.cloudbeaver.auth.provider.fa.AbstractSessionExternal; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.auth.SMAuthSpace; +import org.jkiss.dbeaver.model.auth.SMSession; +import org.jkiss.dbeaver.model.data.json.JSONUtils; + +import java.util.Map; + +public class LdapSession extends AbstractSessionExternal { + protected LdapSession( + @NotNull SMSession parentSession, + @NotNull SMAuthSpace space, + @NotNull Map authParameters + ) { + super(parentSession, space, authParameters); + } + + @NotNull + @Override + public String getSessionId() { + return JSONUtils.getString(authParameters, LdapConstants.CRED_SESSION_ID, "sessionNotFound"); + } +} diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java new file mode 100644 index 0000000000..725879d280 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java @@ -0,0 +1,112 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.ldap.auth; + +import io.cloudbeaver.service.ldap.auth.ssl.LdapSslSetting; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; +import org.jkiss.utils.CommonUtils; + +public class LdapSettings { + @NotNull + private final SMAuthProviderCustomConfiguration providerConfiguration; + @NotNull + private final String host; + @NotNull + private final String baseDN; + private final int port; + @NotNull + private final String userIdentifierAttr; + private final String bindUser; + private final String bindUserPassword; + private final String filter; + private final String loginAttribute; + private final LdapSslSetting ldapSslSetting; + + + public LdapSettings( + SMAuthProviderCustomConfiguration providerConfiguration + ) { + this.providerConfiguration = providerConfiguration; + this.host = providerConfiguration.getParameter(LdapConstants.PARAM_HOST); + this.port = CommonUtils.isNotEmpty(providerConfiguration.getParameter(LdapConstants.PARAM_PORT)) ? Integer.parseInt( + providerConfiguration.getParameter(LdapConstants.PARAM_PORT)) : 389; + this.baseDN = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_DN, ""); + this.userIdentifierAttr = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_USER_IDENTIFIER_ATTR, + "cn"); + this.bindUser = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_BIND_USER, ""); + this.bindUserPassword = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_BIND_USER_PASSWORD, ""); + this.filter = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_FILTER, ""); + this.loginAttribute = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_LOGIN, ""); + this.ldapSslSetting = new LdapSslSetting( + providerConfiguration.getParameter(LdapConstants.PARAM_SSL_ENABLE), + providerConfiguration.getParameter(LdapConstants.PARAM_SSL_CERT) + ); + } + + + @NotNull + public String getBaseDN() { + return baseDN; + } + + @NotNull + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public String getLdapProviderUrl() { + if (ldapSslSetting.isEnable()) { + return "ldaps://" + getHost() + ":" + getPort(); + } + return "ldap://" + getHost() + ":" + getPort(); + } + + @NotNull + public String getUserIdentifierAttr() { + return userIdentifierAttr; + } + + public String getBindUserDN() { + return bindUser; + } + + public String getBindUserPassword() { + return bindUserPassword; + } + + public String getFilter() { + return filter; + } + + public String getLoginAttribute() { + return loginAttribute; + } + + public LdapSslSetting getLdapSslSetting() { + return ldapSslSetting; + } + + @NotNull + public SMAuthProviderCustomConfiguration getProviderConfiguration() { + return providerConfiguration; + } +} diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/ssl/LdapSslSetting.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/ssl/LdapSslSetting.java new file mode 100644 index 0000000000..f0b63f9c96 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/ssl/LdapSslSetting.java @@ -0,0 +1,39 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.ldap.auth.ssl; + +import org.jkiss.code.Nullable; + +public class LdapSslSetting { + private final boolean isEnable; + @Nullable + private final String sslCert; + + public LdapSslSetting(Boolean isEnable, @Nullable String sslCert) { + this.isEnable = Boolean.TRUE.equals(isEnable); + this.sslCert = sslCert; + } + + public boolean isEnable() { + return isEnable; + } + + @Nullable + public String getSslCert() { + return sslCert; + } +} diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/ssl/LdapSslSocketFactory.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/ssl/LdapSslSocketFactory.java new file mode 100644 index 0000000000..9cc0d01732 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/ssl/LdapSslSocketFactory.java @@ -0,0 +1,78 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.ldap.auth.ssl; + +import org.jkiss.code.NotNull; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; + +/** + * This class implementation correspond JNDI api. + * Intention is creating isolated ssl socket factory for each user, not for general keystore + */ +public class LdapSslSocketFactory extends SSLSocketFactory { + private static final ThreadLocal tlsFactory = new ThreadLocal<>(); + + public static void setContextFactory(@NotNull SSLContext ctx) { + tlsFactory.set(ctx.getSocketFactory()); + } + + public static void removeContextFactory() { + tlsFactory.remove(); + } + + //this method is called by internal api + public static SSLSocketFactory getDefault() { + SSLSocketFactory factory = tlsFactory.get(); + if (factory == null) { + throw new IllegalStateException("No SSLContext set in current thread"); + } + return factory; + } + + public String[] getDefaultCipherSuites() { + return getDefault().getDefaultCipherSuites(); + } + + public String[] getSupportedCipherSuites() { + return getDefault().getSupportedCipherSuites(); + } + + public Socket createSocket(Socket s, String h, int p, boolean a) throws IOException { + return getDefault().createSocket(s, h, p, a); + } + + public Socket createSocket(String h, int p) throws IOException { + return getDefault().createSocket(h, p); + } + + public Socket createSocket(String h, int p, InetAddress l, int lp) throws IOException { + return getDefault().createSocket(h, p, l, lp); + } + + public Socket createSocket(InetAddress h, int p) throws IOException { + return getDefault().createSocket(h, p); + } + + public Socket createSocket(InetAddress h, int p, InetAddress l, int lp) throws IOException { + return getDefault().createSocket(h, p, l, lp); + } +} diff --git a/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF index 9bcd574271..f90a9d9e93 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF @@ -3,11 +3,10 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Metadata Bundle-SymbolicName: io.cloudbeaver.service.metadata;singleton:=true -Bundle-Version: 1.0.86.qualifier -Bundle-Release-Date: 20230209 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 1.0.133.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy -Bundle-ClassPath: . Require-Bundle: io.cloudbeaver.server Bundle-Localization: OSGI-INF/l10n/bundle Automatic-Module-Name: io.cloudbeaver.service.metadata diff --git a/server/bundles/io.cloudbeaver.service.metadata/pom.xml b/server/bundles/io.cloudbeaver.service.metadata/pom.xml index 899b6a3cbc..1a87fad8df 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/pom.xml +++ b/server/bundles/io.cloudbeaver.service.metadata/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.metadata - 1.0.86-SNAPSHOT + 1.0.133-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.metadata/schema/service.metadata.graphqls b/server/bundles/io.cloudbeaver.service.metadata/schema/service.metadata.graphqls index 522c950d59..4135299da4 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/schema/service.metadata.graphqls +++ b/server/bundles/io.cloudbeaver.service.metadata/schema/service.metadata.graphqls @@ -3,7 +3,9 @@ extend type Query { # Get child nodes + "Returns node DDL for the specified node ID" metadataGetNodeDDL(nodeId: ID!, options: Object): String + "Returns extended node DDL for the specified node ID (e.g., Oracle or MySQL package)" metadataGetNodeExtendedDDL(nodeId: ID!): String } diff --git a/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/DBWServiceMetadata.java b/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/DBWServiceMetadata.java index f0db1c738b..c8e74a0915 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/DBWServiceMetadata.java +++ b/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/DBWServiceMetadata.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,10 +16,10 @@ */ package io.cloudbeaver.service.metadata; -import io.cloudbeaver.service.DBWService; import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebAction; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.service.DBWService; import org.jkiss.dbeaver.model.navigator.DBNNode; import java.util.Map; diff --git a/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/WebServiceBindingMetadata.java b/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/WebServiceBindingMetadata.java index bcca8fc6de..d135f77666 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/WebServiceBindingMetadata.java +++ b/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/WebServiceBindingMetadata.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -25,8 +25,6 @@ import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.navigator.DBNNode; -import java.util.Map; - /** * Web service implementation */ @@ -54,6 +52,9 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { private DBNNode getNodeFromPath(DataFetchingEnvironment env) throws DBException { WebSession webSession = getWebSession(env); String nodePath = env.getArgument("nodeId"); - return webSession.getNavigatorModel().getNodeByPath(webSession.getProgressMonitor(), nodePath); + if (nodePath == null) { + throw new DBException("Node path is null"); + } + return webSession.getNavigatorModelOrThrow().getNodeByPath(webSession.getProgressMonitor(), nodePath); } } diff --git a/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/impl/WebServiceMetadata.java b/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/impl/WebServiceMetadata.java index 6c6a5fd587..d65f69b877 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/impl/WebServiceMetadata.java +++ b/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/impl/WebServiceMetadata.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -40,7 +40,7 @@ public String getNodeDDL(WebSession webSession, DBNNode dbNode, Map(); @@ -57,7 +57,7 @@ public String getNodeExtendedDDL(WebSession webSession, DBNNode dbNode) throws D validateDatabaseNode(dbNode); DBSObject object = ((DBNDatabaseNode) dbNode).getObject(); if (!(object instanceof DBPScriptObjectExt)) { - throw new DBWebException("Object '" + dbNode.getNodeItemPath() + "' doesn't support extended DDL"); + throw new DBWebException("Object '" + dbNode.getNodeUri() + "' doesn't support extended DDL"); } try { return ((DBPScriptObjectExt) object).getExtendedDefinitionText(webSession.getProgressMonitor()); @@ -68,7 +68,7 @@ public String getNodeExtendedDDL(WebSession webSession, DBNNode dbNode) throws D private void validateDatabaseNode(DBNNode dbNode) throws DBWebException { if (!(dbNode instanceof DBNDatabaseNode)) { - throw new DBWebException("Node '" + dbNode.getNodeItemPath() + "' is not database node"); + throw new DBWebException("Node '" + dbNode.getNodeUri() + "' is not database node"); } } } diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF index 25d8f12537..1ec8599c00 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF @@ -3,11 +3,10 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Resource manager NIO implementation Bundle-SymbolicName: io.cloudbeaver.service.rm.nio;singleton:=true -Bundle-Version: 1.0.0.qualifier -Bundle-Release-Date: 20230927 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 1.0.47.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy -Bundle-ClassPath: . Require-Bundle: io.cloudbeaver.model, org.jkiss.dbeaver.model, org.jkiss.dbeaver.model.nio diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/plugin.xml b/server/bundles/io.cloudbeaver.service.rm.nio/plugin.xml index d6694efc2c..c3e1b00a44 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.rm.nio/plugin.xml @@ -3,10 +3,10 @@ - - + + - - + + diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml b/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml index 2aefd764ac..50ea0bdd25 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml +++ b/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.rm.nio - 1.0.0-SNAPSHOT + 1.0.47-SNAPSHOT eclipse-plugin 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 49b68bfd67..53c60d23b1 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,27 +16,29 @@ */ 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.AbstractVirtualFileSystem; 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; import java.net.URI; import java.nio.file.Path; +import java.util.List; -public class RMVirtualFileSystem implements DBFVirtualFileSystem { +public class RMVirtualFileSystem extends AbstractVirtualFileSystem { @NotNull private final RMNIOFileSystemProvider rmNioFileSystemProvider; @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; } @@ -68,14 +70,21 @@ public String getId() { return rmProject.getId(); } + @NotNull + @Override + public String getProviderId() { + return "rm-nio"; + } + + @NotNull @Override - public Path getPathByURI(DBRProgressMonitor monitor, URI uri) { + public Path getPathByURI(@NotNull DBRProgressMonitor monitor, @NotNull URI uri) { return rmNioFileSystemProvider.getPath(uri); } @NotNull @Override - public DBFVirtualFileSystemRoot[] getRootFolders(DBRProgressMonitor monitor) throws DBException { - return new RMVirtualFileSystemRoot[]{new RMVirtualFileSystemRoot(this, rmProject, rmNioFileSystemProvider)}; + public List getRootFolders(DBRProgressMonitor monitor) throws DBException { + return List.of(new RMVirtualFileSystemRoot(this, rmProject, rmNioFileSystemProvider)); } } 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..4291cb47c0 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,16 +16,15 @@ */ 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.AbstractFileSystemProvider; 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 { +public class RMVirtualFileSystemProvider extends AbstractFileSystemProvider { private static final Log log = Log.getLog(RMVirtualFileSystemProvider.class); @Override @@ -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())}; } } diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemRoot.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemRoot.java index 10f9ad6d9b..8c5fa923f8 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemRoot.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemRoot.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,8 +16,8 @@ */ package io.cloudbeaver.service.rm.fs; -import io.cloudbeaver.service.rm.nio.RMNIOFileSystemProvider; import io.cloudbeaver.service.rm.nio.RMNIOFileSystem; +import io.cloudbeaver.service.rm.nio.RMNIOFileSystemProvider; import io.cloudbeaver.service.rm.nio.RMPath; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.DBPImage; diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystem.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystem.java index f0ed593597..36c1362843 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystem.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystem.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. 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..a8cc0e0a6d 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -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/RMNIOProjectFileStore.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOProjectFileStore.java index 2e08f5f0a8..0d0fc3757e 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOProjectFileStore.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOProjectFileStore.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMOutputStream.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMOutputStream.java index 4db2a79a69..732474803a 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMOutputStream.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMOutputStream.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. 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..9f9b97654e 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -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()) { diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMResourceBasicAttribute.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMResourceBasicAttribute.java index cff20b301c..9742c2d7c4 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMResourceBasicAttribute.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMResourceBasicAttribute.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMRootBasicAttribute.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMRootBasicAttribute.java index 7cb22e8123..315e5a474a 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMRootBasicAttribute.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMRootBasicAttribute.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. diff --git a/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF index eaa40f9131..f06d800124 100644 --- a/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF @@ -3,11 +3,10 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Resource manager Bundle-SymbolicName: io.cloudbeaver.service.rm;singleton:=true -Bundle-Version: 1.0.35.qualifier -Bundle-Release-Date: 20230209 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 1.0.82.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy -Bundle-ClassPath: . Require-Bundle: io.cloudbeaver.server, io.cloudbeaver.service.admin Automatic-Module-Name: io.cloudbeaver.service.rm diff --git a/server/bundles/io.cloudbeaver.service.rm/pom.xml b/server/bundles/io.cloudbeaver.service.rm/pom.xml index 1da5782e91..71cccc7e74 100644 --- a/server/bundles/io.cloudbeaver.service.rm/pom.xml +++ b/server/bundles/io.cloudbeaver.service.rm/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.rm - 1.0.35-SNAPSHOT + 1.0.82-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls index 79589bb1fa..005470b9b7 100644 --- a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls +++ b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls @@ -21,14 +21,6 @@ type RMResource { properties: Object } -type RMResourceType { - id: String! - displayName: String! - icon: String - fileExtensions: [String!]! - rootFolder: String -} - input RMSubjectProjectPermissions { subjectId: String! permissions: [String!]! @@ -42,65 +34,93 @@ input RMProjectPermissions { extend type Query { - # List accessible projects + "List accessible projects for a current user" rmListProjects: [RMProject!]! + "List shared projects for a current user" rmListSharedProjects: [RMProject!]! + "Returns project information by projectId" rmProject(projectId: String!): RMProject! + "Returns available project permissions. Can be read only by users with admin permissions" rmListProjectPermissions: [AdminPermissionInfo!]! + "Returns project permissions for the specified project. Can be read only by users with admin permissions" rmListProjectGrantedPermissions(projectId: String!): [AdminObjectGrantInfo!]! + "Returns all project grants bysubjectId. Can be read only by users with admin permissions" rmListSubjectProjectsPermissionGrants(subjectId: String!): [AdminObjectGrantInfo!]! + "Returns resources in the specified project and folder. If folder is not specified, returns resources in the root folder" rmListResources( projectId: String!, folder: String, nameMask: String, readProperties: Boolean, - readHistory: Boolean): [RMResource!]! @deprecated + readHistory: Boolean + ): [RMResource!]! - # Reads resource contents as string in UTF-8 + "Reads resource contents as string in UTF-8" rmReadResourceAsString( projectId: String!, - resourcePath: String!): String! @deprecated + resourcePath: String! + ): String! } extend type Mutation { + "Creates a new resource in the specified project and folder. If isFolder is true then creates a folder, otherwise creates a file" rmCreateResource( projectId: String!, resourcePath: String!, - isFolder: Boolean!): String! @deprecated + isFolder: Boolean! + ): String! + "Moves resource to the specified new path in the same project. Can be used to rename a resource" rmMoveResource( projectId: String!, oldResourcePath: String!, - newResourcePath: String): String! @deprecated + newResourcePath: String + ): String! + "Deletes resource by path in the specified project. If recursive is true then deletes all sub-resources in the folder" rmDeleteResource( projectId: String!, resourcePath: String!, - recursive: Boolean!): Boolean @deprecated + recursive: Boolean! + ): Boolean + """ + Writes string content to the resource. + If forceOverwrite is true then overwrites existing resource, otherwise throws an error if resource already exists + """ rmWriteResourceStringContent( projectId: String!, resourcePath: String!, data: String!, - forceOverwrite: Boolean!): String! @deprecated + forceOverwrite: Boolean! + ): String! + "Creates a new project with the specified projectId and projectName." rmCreateProject( projectId: ID, projectName: String!, - description: String): RMProject! @deprecated + description: String + ): RMProject! + "Deletes project by projectId. Returns true if project was deleted, false if project not found" rmDeleteProject(projectId: ID!): Boolean! - rmSetProjectPermissions(projectId: String!, permissions: [RMSubjectProjectPermissions!]!): Boolean! + rmSetProjectPermissions(projectId: String!, permissions: [RMSubjectProjectPermissions!]!): Boolean! @deprecated(reason: "use setConnectionSubjectAccess") + + rmSetSubjectProjectPermissions(subjectId: String!, permissions: [RMProjectPermissions!]!): Boolean! @deprecated - rmSetSubjectProjectPermissions(subjectId: String!, permissions: [RMProjectPermissions!]!): Boolean! + "Adds project permissions to the specified projects based on subject IDs and permissions. Returns true if permissions were added successfully." + rmAddProjectsPermissions(projectIds: [ID!]!, subjectIds: [ID!]!, permissions:[String!]! ): Boolean @since(version: "23.2.2") + "Deletes project permissions from the specified projects based on subject IDs and permissions. Returns true if permissions were deleted successfully." + rmDeleteProjectsPermissions(projectIds: [ID!]!, subjectIds: [ID!]!, permissions:[String!]!): Boolean @since(version: "23.2.2") + "Sets resource property by name. If value is null then removes the property (e.g., sets relation between resource and connection)." rmSetResourceProperty(projectId: String!, resourcePath: String!, name: ID!, value: String): Boolean! } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java index 355fe25f12..742128b5c8 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -136,6 +136,7 @@ boolean deleteProject( @WebAction(requirePermissions = {RMConstants.PERMISSION_RM_ADMIN}) List listProjectPermissions() throws DBWebException; + @Deprecated @WebProjectAction( requireProjectPermissions = RMConstants.PERMISSION_PROJECT_ADMIN ) @@ -145,6 +146,7 @@ boolean setProjectPermissions( @NotNull RMSubjectProjectPermissions projectPermissions ) throws DBWebException; + @Deprecated @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) boolean setSubjectProjectPermissions( @NotNull WebSession webSession, @@ -152,6 +154,22 @@ boolean setSubjectProjectPermissions( @NotNull RMProjectPermissions projectPermissions ) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + boolean deleteProjectsPermissions( + @NotNull WebSession webSession, + @NotNull List projectIds, + @NotNull List subjectIds, + @NotNull List permissions + ) throws DBWebException; + + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + boolean addProjectsPermissions( + @NotNull WebSession webSession, + @NotNull List projectIds, + @NotNull List subjectIds, + @NotNull List permissions + ) throws DBWebException; + @WebProjectAction( requireProjectPermissions = RMConstants.PERMISSION_PROJECT_ADMIN ) diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/RMNavigatorModelExtender.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/RMNavigatorModelExtender.java index 9625df1bae..b39b3951cb 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/RMNavigatorModelExtender.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/RMNavigatorModelExtender.java @@ -1,24 +1,24 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp + * Copyright (C) 2010-2024 DBeaver Corp and others * - * All Rights Reserved. + * 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 * - * 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. + * 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.rm; import io.cloudbeaver.model.rm.DBNResourceManagerRoot; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.utils.ServletAppUtils; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.navigator.DBNModelExtender; import org.jkiss.dbeaver.model.navigator.DBNNode; @@ -28,7 +28,9 @@ public class RMNavigatorModelExtender implements DBNModelExtender { @Override public DBNNode[] getExtraNodes(@NotNull DBNNode parentNode) { - if (parentNode instanceof DBNRoot && WebAppUtils.getWebApplication().getAppConfiguration().isResourceManagerEnabled()) { + if (parentNode instanceof DBNRoot && ServletAppUtils.getServletApplication() + .getAppConfiguration() + .isResourceManagerEnabled()) { return createRMNodes((DBNRoot) parentNode); } else { return null; diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java index fac21fac06..f3dfcbb3be 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -112,6 +112,18 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env.getArgument("subjectId"), new RMProjectPermissions(env.getArgument("permissions")) )) + .dataFetcher("rmAddProjectsPermissions", env -> getService(env).addProjectsPermissions( + getWebSession(env), + env.getArgument("projectIds"), + env.getArgument("subjectIds"), + env.getArgument("permissions") + )) + .dataFetcher("rmDeleteProjectsPermissions", env -> getService(env).deleteProjectsPermissions( + getWebSession(env), + env.getArgument("projectIds"), + env.getArgument("subjectIds"), + env.getArgument("permissions") + )) ; } } diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java index 96ade14e07..be715f71ef 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -23,14 +23,16 @@ import io.cloudbeaver.service.rm.model.RMProjectPermissions; import io.cloudbeaver.service.rm.model.RMSubjectProjectPermissions; import io.cloudbeaver.service.security.SMUtils; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.utils.ServletAppUtils; import io.cloudbeaver.utils.WebEventUtils; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.rm.RMResource; +import org.jkiss.dbeaver.model.secret.DBSSecretController; import org.jkiss.dbeaver.model.security.*; import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent; @@ -38,6 +40,7 @@ import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceProperty; import java.nio.charset.StandardCharsets; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -47,6 +50,8 @@ */ public class WebServiceRM implements DBWServiceRM { + private static final Log log = Log.getLog(WebServiceRM.class); + @Override public RMProject[] listProjects(@NotNull WebSession webSession) throws DBWebException { try { @@ -103,7 +108,7 @@ public RMResource[] listResources(@NotNull WebSession webSession, * @param resourcePath the resource path * @param propertyName the property name * @param propertyValue the property value - * @return the resource property + * @return true on success * @throws DBException the db exception */ @NotNull @@ -212,7 +217,7 @@ public boolean moveResource(@NotNull WebSession webSession, WSResourceProperty.NAME); return true; } catch (Exception e) { - throw new DBWebException("Error moving resource " + oldResourcePath, e); + throw new DBWebException(e.getMessage(), e); } } @@ -252,7 +257,7 @@ public RMProject createProject( try { RMProject rmProject = getResourceController(session).createProject(name, description); session.addSessionProject(rmProject.getId()); - WebAppUtils.getWebApplication().getEventController().addEvent( + ServletAppUtils.getServletApplication().getEventController().addEvent( WSProjectUpdateEvent.create(session.getSessionId(), session.getUserId(), rmProject.getId()) ); return rmProject; @@ -264,9 +269,17 @@ public RMProject createProject( @Override public boolean deleteProject(@NotNull WebSession session, @NotNull String projectId) throws DBWebException { try { + var project = session.getProjectById(projectId); + if (project == null) { + throw new DBException("Project not found: " + projectId); + } + if (project.isUseSecretStorage()) { + var secretController = DBSSecretController.getProjectSecretController(project); + secretController.deleteProjectSecrets(project.getId()); + } getResourceController(session).deleteProject(projectId); session.removeSessionProject(projectId); - WebAppUtils.getWebApplication().getEventController().addEvent( + ServletAppUtils.getServletApplication().getEventController().addEvent( WSProjectUpdateEvent.delete(session.getSessionId(), session.getUserId(), projectId) ); return true; @@ -349,6 +362,63 @@ public boolean setSubjectProjectPermissions( } } + @Override + public boolean deleteProjectsPermissions( + @NotNull WebSession webSession, + @NotNull List projectIds, + @NotNull List subjectIds, + @NotNull List permissions + ) throws DBWebException { + try { + SMAdminController smAdminController = webSession.getAdminSecurityController(); + smAdminController.deleteObjectPermissions( + new HashSet<>(projectIds), + SMObjectType.project, + new HashSet<>(subjectIds), + new HashSet<>(permissions) + ); + log.info("Project permissions deleted: [projectIds=%s, subjectIds=%s, permissions=%s, madeBy=%s]" + .formatted( + String.join(",", projectIds), + String.join(",", subjectIds), + String.join(",", permissions), + webSession.getUserId() + )); + return true; + } catch (Exception e) { + throw new DBWebException("Error deleting project permissions", e); + } + } + + @Override + public boolean addProjectsPermissions( + @NotNull WebSession webSession, + @NotNull List projectIds, + @NotNull List subjectIds, + @NotNull List permissions + ) throws DBWebException { + try { + SMAdminController smAdminController = webSession.getAdminSecurityController(); + smAdminController.addObjectPermissions( + new HashSet<>(projectIds), + SMObjectType.project, + new HashSet<>(subjectIds), + new HashSet<>(permissions), + webSession.getUserId() + ); + log.info("Project permissions added: [projectIds=%s, subjectIds=%s, permissions=%s, madeBy=%s]" + .formatted( + String.join(",", projectIds), + String.join(",", subjectIds), + String.join(",", permissions), + webSession.getUserId() + )); + return true; + } catch (Exception e) { + throw new DBWebException("Error adding project permissions", e); + } + } + @Override public List listProjectGrantedPermissions(@NotNull WebSession webSession, @NotNull String projectId diff --git a/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF index 9577a40f07..64cbf369e1 100644 --- a/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF @@ -1,21 +1,22 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 -Bundle-Vendor: Cloudbeaver Web Service - Security +Bundle-Name: Cloudbeaver Web Service - Security Bundle-Vendor: DBeaver Corp Bundle-SymbolicName: io.cloudbeaver.service.security;singleton:=true -Bundle-Version: 1.0.38.qualifier -Bundle-Release-Date: 20230209 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 1.0.85.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy -Bundle-ClassPath: . Require-Bundle: org.jkiss.dbeaver.model;visibility:=reexport, org.jkiss.dbeaver.model.jdbc, org.jkiss.dbeaver.model.sql, + org.jkiss.dbeaver.model.sql.jdbc;visibility:=reexport, org.jkiss.dbeaver.registry;visibility:=reexport, - org.jkiss.bundle.apache.dbcp, + org.jkiss.bundle.apache.dbcp;visibility:=reexport, io.cloudbeaver.model Export-Package: io.cloudbeaver.auth.provider.local, io.cloudbeaver.auth.provider.rp, io.cloudbeaver.service.security, io.cloudbeaver.service.security.db +Bundle-Localization: OSGI-INF/l10n/bundle Automatic-Module-Name: io.cloudbeaver.service.security diff --git a/server/bundles/io.cloudbeaver.service.security/OSGI-INF/l10n/bundle.properties b/server/bundles/io.cloudbeaver.service.security/OSGI-INF/l10n/bundle.properties new file mode 100644 index 0000000000..c871cdc03a --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/OSGI-INF/l10n/bundle.properties @@ -0,0 +1,16 @@ +prop.auth.model.reverseProxy.logout-url = Logout URL +prop.auth.model.reverseProxy.logout-url.description = Logout URL +prop.auth.model.reverseProxy.user-header = Username header +prop.auth.model.reverseProxy.user-header.description = Username header +prop.auth.model.reverseProxy.team-header = Team header +prop.auth.model.reverseProxy.team-header.description = Team header +prop.auth.model.reverseProxy.team-delimiter = Team delimiter symbol +prop.auth.model.reverseProxy.team-delimiter.description = Team delimiter symbol, default: | +prop.auth.model.reverseProxy.first-name-header = First name header +prop.auth.model.reverseProxy.first-name-header.description = First name header name +prop.auth.model.reverseProxy.last-name-header = Last name header +prop.auth.model.reverseProxy.last-name-header.description = Last name header name +prop.auth.model.reverseProxy.full-name-header = Full name header +prop.auth.model.reverseProxy.full-name-header.description = Full name header name +prop.auth.model.reverseProxy.role-header = Role header +prop.auth.model.reverseProxy.role-header.description = Role header name diff --git a/server/bundles/io.cloudbeaver.service.security/OSGI-INF/l10n/bundle_ru.properties b/server/bundles/io.cloudbeaver.service.security/OSGI-INF/l10n/bundle_ru.properties new file mode 100644 index 0000000000..805afb2031 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/OSGI-INF/l10n/bundle_ru.properties @@ -0,0 +1,16 @@ +prop.auth.model.reverseProxy.logout-url = URL-адрес выхода из системы +prop.auth.model.reverseProxy.logout-url.description = URL-адрес выхода из системы +prop.auth.model.reverseProxy.user-header = Заголовок имени пользователя +prop.auth.model.reverseProxy.user-header.description = Заголовок имени пользователя +prop.auth.model.reverseProxy.team-header = Заголовок команды +prop.auth.model.reverseProxy.team-header.description = Заголовок команды +prop.auth.model.reverseProxy.team-delimiter = Символ разделителя команд +prop.auth.model.reverseProxy.team-delimiter.description = Символ разделителя команд, по умолчанию: | +prop.auth.model.reverseProxy.first-name-header = Заголовок имени +prop.auth.model.reverseProxy.first-name-header.description = Название заголовка имени +prop.auth.model.reverseProxy.last-name-header = Заголовок фамилии +prop.auth.model.reverseProxy.last-name-header.description = Имя заголовка фамилии +prop.auth.model.reverseProxy.full-name-header = Заголовок полного имени +prop.auth.model.reverseProxy.full-name-header.description = Имя заголовка полного имени +prop.auth.model.reverseProxy.role-header = Заголовок роли +prop.auth.model.reverseProxy.role-header.description = Имя заголовка роли \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/build.properties b/server/bundles/io.cloudbeaver.service.security/build.properties index d9f1a1b5a9..261610303e 100644 --- a/server/bundles/io.cloudbeaver.service.security/build.properties +++ b/server/bundles/io.cloudbeaver.service.security/build.properties @@ -2,5 +2,6 @@ source.. = src/ output.. = target/classes/ bin.includes = .,\ META-INF/,\ + OSGI-INF/,\ db/,\ plugin.xml diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql index 2896c125c2..524cfb0c82 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql @@ -1,7 +1,8 @@ CREATE TABLE {table_prefix}CB_SCHEMA_INFO ( - VERSION INTEGER NOT NULL, - UPDATE_TIME TIMESTAMP NOT NULL + MODULE_ID VARCHAR(10) NOT NULL, + VERSION INTEGER NOT NULL, + UPDATE_TIME TIMESTAMP NOT NULL ); CREATE TABLE {table_prefix}CB_INSTANCE @@ -30,38 +31,11 @@ CREATE TABLE {table_prefix}CB_INSTANCE_DETAILS FOREIGN KEY (INSTANCE_ID) REFERENCES {table_prefix}CB_INSTANCE (INSTANCE_ID) ); -CREATE TABLE {table_prefix}CB_INSTANCE_EVENT -( - EVENT_ID BIGINT AUTO_INCREMENT NOT NULL, - - INSTANCE_ID CHAR(36) NOT NULL, -- Unique instance ID - - EVENT_TYPE VARCHAR(16) NOT NULL, - EVENT_TIME TIMESTAMP NOT NULL, - - EVENT_MESSAGE VARCHAR(255), - - PRIMARY KEY (EVENT_ID), - FOREIGN KEY (INSTANCE_ID) REFERENCES {table_prefix}CB_INSTANCE (INSTANCE_ID) -); - -CREATE TABLE {table_prefix}CB_WORKSPACE -( - WORKSPACE_ID VARCHAR(32) NOT NULL, -- Workspace unique ID - INSTANCE_ID CHAR(36) NOT NULL, -- Unique instance ID - - WORKSPACE_LOCATION VARCHAR(1024) NOT NULL, - - UPDATE_TIME TIMESTAMP NOT NULL, - - PRIMARY KEY (INSTANCE_ID, WORKSPACE_ID), - FOREIGN KEY (INSTANCE_ID) REFERENCES {table_prefix}CB_INSTANCE (INSTANCE_ID) -); - CREATE TABLE {table_prefix}CB_AUTH_SUBJECT ( - SUBJECT_ID VARCHAR(128) NOT NULL, - SUBJECT_TYPE VARCHAR(8) NOT NULL, + SUBJECT_ID VARCHAR(128) NOT NULL, + SUBJECT_TYPE VARCHAR(8) NOT NULL, + IS_SECRET_STORAGE CHAR(1) DEFAULT 'Y' NOT NULL, PRIMARY KEY (SUBJECT_ID) ); @@ -105,56 +79,62 @@ CREATE TABLE {table_prefix}CB_OBJECT_PERMISSIONS FOREIGN KEY (SUBJECT_ID) REFERENCES {table_prefix}CB_AUTH_SUBJECT (SUBJECT_ID) ON DELETE CASCADE ); +CREATE TABLE {table_prefix}CB_CREDENTIALS_PROFILE +( + PROFILE_ID VARCHAR(128) NOT NULL, + PROFILE_NAME VARCHAR(100) NOT NULL, + PROFILE_DESCRIPTION VARCHAR(255), + PARENT_PROFILE_ID VARCHAR(128), + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + + PRIMARY KEY (PROFILE_ID), + FOREIGN KEY (PROFILE_ID) REFERENCES {table_prefix}CB_AUTH_SUBJECT (SUBJECT_ID) ON DELETE CASCADE, + FOREIGN KEY (PARENT_PROFILE_ID) REFERENCES {table_prefix}CB_CREDENTIALS_PROFILE(PROFILE_ID) ON DELETE NO ACTION +); + CREATE TABLE {table_prefix}CB_USER ( USER_ID VARCHAR(128) NOT NULL, - IS_ACTIVE CHAR(1) NOT NULL, CREATE_TIME TIMESTAMP NOT NULL, DEFAULT_AUTH_ROLE VARCHAR(32) NULL, + CREDENTIALS_PROFILE_ID VARCHAR(128) NULL, + CHANGE_DATE TIMESTAMP NULL, + DISABLED_BY VARCHAR(128) NULL, + DISABLE_REASON VARCHAR(128) NULL, PRIMARY KEY (USER_ID), - FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_AUTH_SUBJECT (SUBJECT_ID) ON DELETE CASCADE + FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_AUTH_SUBJECT (SUBJECT_ID) ON DELETE CASCADE, + FOREIGN KEY (CREDENTIALS_PROFILE_ID) REFERENCES {table_prefix}CB_CREDENTIALS_PROFILE(PROFILE_ID) ON DELETE NO ACTION ); -- Additional user properties (profile) - -CREATE TABLE {table_prefix}CB_USER_PARAMETERS +CREATE TABLE {table_prefix}CB_USER_PREFERENCES ( USER_ID VARCHAR(128) NOT NULL, - PARAM_ID VARCHAR(32) NOT NULL, - PARAM_VALUE VARCHAR(1024), + PREFERENCE_ID VARCHAR(128) NOT NULL, + PREFERENCE_VALUE VARCHAR(1024), - PRIMARY KEY (USER_ID, PARAM_ID), + PRIMARY KEY (USER_ID, PREFERENCE_ID), FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_USER (USER_ID) ON DELETE CASCADE ); CREATE TABLE {table_prefix}CB_TEAM ( - TEAM_ID VARCHAR(128) NOT NULL, - TEAM_NAME VARCHAR(100) NOT NULL, - TEAM_DESCRIPTION VARCHAR(255) NOT NULL, - - CREATE_TIME TIMESTAMP NOT NULL, + TEAM_ID VARCHAR(128) NOT NULL, + TEAM_NAME VARCHAR(100) NOT NULL, + TEAM_DESCRIPTION VARCHAR(255), + CREATE_TIME TIMESTAMP NOT NULL, PRIMARY KEY (TEAM_ID), FOREIGN KEY (TEAM_ID) REFERENCES {table_prefix}CB_AUTH_SUBJECT (SUBJECT_ID) ON DELETE CASCADE ); -CREATE TABLE {table_prefix}CB_EXTERNAL_TEAM -( - TEAM_ID VARCHAR(128) NOT NULL, - EXTERNAL_TEAM_ID VARCHAR(128) NOT NULL, - - PRIMARY KEY (TEAM_ID,EXTERNAL_TEAM_ID), - FOREIGN KEY (TEAM_ID) REFERENCES {table_prefix}CB_TEAM (TEAM_ID) ON DELETE CASCADE -); - - CREATE TABLE {table_prefix}CB_USER_TEAM ( USER_ID VARCHAR(128) NOT NULL, TEAM_ID VARCHAR(128) NOT NULL, + TEAM_ROLE VARCHAR(128), GRANT_TIME TIMESTAMP NOT NULL, GRANTED_BY VARCHAR(128) NOT NULL, @@ -164,24 +144,6 @@ CREATE TABLE {table_prefix}CB_USER_TEAM FOREIGN KEY (TEAM_ID) REFERENCES {table_prefix}CB_TEAM (TEAM_ID) ON DELETE NO ACTION ); -CREATE TABLE {table_prefix}CB_AUTH_PROVIDER -( - PROVIDER_ID VARCHAR(32) NOT NULL, - IS_ENABLED CHAR(1) NOT NULL, - - PRIMARY KEY (PROVIDER_ID) -); - -CREATE TABLE {table_prefix}CB_AUTH_CONFIGURATION -( - PROVIDER_ID VARCHAR(32) NOT NULL, - PARAM_ID VARCHAR(32) NOT NULL, - PARAM_VALUE VARCHAR(1024), - - PRIMARY KEY (PROVIDER_ID, PARAM_ID), - FOREIGN KEY (PROVIDER_ID) REFERENCES {table_prefix}CB_AUTH_PROVIDER (PROVIDER_ID) ON DELETE CASCADE -); - CREATE TABLE {table_prefix}CB_USER_CREDENTIALS ( USER_ID VARCHAR(128) NOT NULL, @@ -228,28 +190,6 @@ CREATE TABLE {table_prefix}CB_SESSION FOREIGN KEY (LAST_ACCESS_INSTANCE_ID) REFERENCES {table_prefix}CB_INSTANCE (INSTANCE_ID) ); -CREATE TABLE {table_prefix}CB_SESSION_STATE -( - SESSION_ID VARCHAR(64) NOT NULL, - - SESSION_STATE TEXT NOT NULL, - UPDATE_TIME TIMESTAMP NOT NULL, - - PRIMARY KEY (SESSION_ID), - FOREIGN KEY (SESSION_ID) REFERENCES {table_prefix}CB_SESSION (SESSION_ID) ON DELETE CASCADE -); - -CREATE TABLE {table_prefix}CB_SESSION_LOG -( - SESSION_ID VARCHAR(64) NOT NULL, - - LOG_TIME TIMESTAMP NOT NULL, - LOG_ACTION VARCHAR(128) NOT NULL, - LOG_DETAILS VARCHAR(255) NOT NULL, - - FOREIGN KEY (SESSION_ID) REFERENCES {table_prefix}CB_SESSION (SESSION_ID) ON DELETE CASCADE -); - CREATE TABLE {table_prefix}CB_AUTH_TOKEN ( TOKEN_ID VARCHAR(128) NOT NULL, @@ -257,6 +197,7 @@ CREATE TABLE {table_prefix}CB_AUTH_TOKEN SESSION_ID VARCHAR(64) NOT NULL, USER_ID VARCHAR(128), AUTH_ROLE VARCHAR(32), + IS_SERVICE CHAR(1) DEFAULT 'N' NOT NULL, EXPIRATION_TIME TIMESTAMP NOT NULL, REFRESH_TOKEN_EXPIRATION_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, @@ -276,7 +217,11 @@ CREATE TABLE {table_prefix}CB_AUTH_ATTEMPT SESSION_ID VARCHAR(64), SESSION_TYPE VARCHAR(64) NOT NULL, APP_SESSION_STATE TEXT NOT NULL, - + IS_MAIN_AUTH CHAR(1) DEFAULT 'Y' NOT NULL, + IS_SERVICE_AUTH CHAR(1) DEFAULT 'N' NOT NULL, + AUTH_USERNAME VARCHAR(128) NULL, + ERROR_CODE VARCHAR(128) NULL, + FORCE_SESSION_LOGOUT CHAR(1) DEFAULT 'N' NOT NULL, CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (AUTH_ID), @@ -295,22 +240,3 @@ CREATE TABLE {table_prefix}CB_AUTH_ATTEMPT_INFO PRIMARY KEY (AUTH_ID, AUTH_PROVIDER_ID), FOREIGN KEY (AUTH_ID) REFERENCES {table_prefix}CB_AUTH_ATTEMPT (AUTH_ID) ON DELETE CASCADE ); - -CREATE INDEX CB_SESSION_LOG_INDEX ON {table_prefix}CB_SESSION_LOG (SESSION_ID, LOG_TIME); - --- Secrets - -CREATE TABLE {table_prefix}CB_USER_SECRETS -( - USER_ID VARCHAR(128) NOT NULL, - SECRET_ID VARCHAR(512) NOT NULL, - SECRET_VALUE TEXT NOT NULL, - - SECRET_LABEL VARCHAR(128), - SECRET_DESCRIPTION VARCHAR(1024), - ENCODING_TYPE VARCHAR(32) NOT NULL DEFAULT 'PLAINTEXT', - UPDATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - - PRIMARY KEY (USER_ID, SECRET_ID), - FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_USER (USER_ID) ON DELETE CASCADE -); diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_10.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_10.sql index 7fdc12e8d0..4f3fb877a0 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_10.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_10.sql @@ -7,11 +7,10 @@ CREATE TABLE {table_prefix}CB_USER_SECRETS SECRET_LABEL VARCHAR(128), SECRET_DESCRIPTION VARCHAR(1024), - UPDATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UPDATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (USER_ID, SECRET_ID), FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_USER (USER_ID) ON DELETE CASCADE ); CREATE INDEX CB_USER_SECRETS_ID ON {table_prefix}CB_USER_SECRETS (USER_ID,SECRET_ID); - diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_11.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_11.sql index 34d76049b9..05405ff3f0 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_11.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_11.sql @@ -1,12 +1,12 @@ -ALTER TABLE {table_prefix}CB_AUTH_TOKEN ADD COLUMN AUTH_ROLE VARCHAR(32); +ALTER TABLE {table_prefix}CB_AUTH_TOKEN ADD AUTH_ROLE VARCHAR(32); -ALTER TABLE {table_prefix}CB_USER ADD COLUMN DEFAULT_AUTH_ROLE VARCHAR(32) NULL; +ALTER TABLE {table_prefix}CB_USER ADD DEFAULT_AUTH_ROLE VARCHAR(32) NULL; CREATE TABLE {table_prefix}CB_TEAM ( TEAM_ID VARCHAR(128) NOT NULL, TEAM_NAME VARCHAR(100) NOT NULL, - TEAM_DESCRIPTION VARCHAR(255) NOT NULL, + TEAM_DESCRIPTION VARCHAR(255), CREATE_TIME TIMESTAMP NOT NULL, diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_13.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_13.sql index 843ab47a31..f76655eee0 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_13.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_13.sql @@ -1,2 +1,2 @@ ALTER TABLE {table_prefix}CB_USER_SECRETS - ADD COLUMN ENCODING_TYPE VARCHAR(32) NOT NULL DEFAULT 'PLAINTEXT'; \ No newline at end of file + ADD ENCODING_TYPE VARCHAR(32) DEFAULT 'PLAINTEXT' NOT NULL; \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_14.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_14.sql new file mode 100644 index 0000000000..743f481099 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_14.sql @@ -0,0 +1,2 @@ +ALTER TABLE {table_prefix}CB_AUTH_ATTEMPT + ADD IS_MAIN_AUTH CHAR(1) DEFAULT 'Y' NOT NULL; \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_15.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_15.sql new file mode 100644 index 0000000000..4f1add7d8f --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_15.sql @@ -0,0 +1,2 @@ +ALTER TABLE {table_prefix}CB_AUTH_ATTEMPT + ADD AUTH_USERNAME VARCHAR(128) NULL; \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_16.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_16.sql new file mode 100644 index 0000000000..71db4cc431 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_16.sql @@ -0,0 +1,29 @@ +CREATE TABLE {table_prefix}CB_SUBJECT_SECRETS +( + SUBJECT_ID VARCHAR(128) NOT NULL, + SECRET_ID VARCHAR(255) NOT NULL, + + PROJECT_ID VARCHAR(128), + OBJECT_TYPE VARCHAR(32), + OBJECT_ID VARCHAR(128), + + SECRET_VALUE TEXT NOT NULL, + + ENCODING_TYPE VARCHAR(32) DEFAULT 'PLAINTEXT' NOT NULL, + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + UPDATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + + PRIMARY KEY (SUBJECT_ID, SECRET_ID), + FOREIGN KEY (SUBJECT_ID) REFERENCES {table_prefix}CB_AUTH_SUBJECT (SUBJECT_ID) ON DELETE CASCADE + ); + +CREATE INDEX IDX_SUBJECT_SECRETS_PROJECT ON {table_prefix}CB_SUBJECT_SECRETS (PROJECT_ID,SUBJECT_ID); +CREATE INDEX IDX_SUBJECT_SECRETS_OBJECT ON {table_prefix}CB_SUBJECT_SECRETS (PROJECT_ID,OBJECT_TYPE,OBJECT_ID); + +INSERT INTO {table_prefix}CB_SUBJECT_SECRETS (SUBJECT_ID, SECRET_ID, SECRET_VALUE, ENCODING_TYPE, CREATE_TIME, UPDATE_TIME) +SELECT USER_ID, SECRET_ID, SECRET_VALUE, ENCODING_TYPE, UPDATE_TIME, UPDATE_TIME FROM {table_prefix}CB_USER_SECRETS; + +DROP TABLE {table_prefix}CB_USER_SECRETS; + +ALTER TABLE {table_prefix}CB_AUTH_SUBJECT + ADD IS_SECRET_STORAGE CHAR(1) DEFAULT 'Y' NOT NULL; \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_17.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_17.sql new file mode 100644 index 0000000000..f2bea8bbf2 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_17.sql @@ -0,0 +1,7 @@ +DROP TABLE {table_prefix}CB_EXTERNAL_TEAM; +DROP TABLE {table_prefix}CB_SESSION_STATE; +DROP TABLE {table_prefix}CB_SESSION_LOG; +DROP TABLE {table_prefix}CB_INSTANCE_EVENT; +DROP TABLE {table_prefix}CB_WORKSPACE; +DROP TABLE {table_prefix}CB_AUTH_CONFIGURATION; +DROP TABLE {table_prefix}CB_AUTH_PROVIDER; diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_17_postgresql.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_17_postgresql.sql new file mode 100644 index 0000000000..69a33c9f74 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_17_postgresql.sql @@ -0,0 +1,8 @@ +DROP TABLE {table_prefix}CB_EXTERNAL_TEAM; +DROP TABLE {table_prefix}CB_SESSION_STATE; +DROP TABLE {table_prefix}CB_SESSION_LOG; +DROP TABLE {table_prefix}CB_INSTANCE_EVENT; +DROP TABLE {table_prefix}CB_WORKSPACE; +DROP TABLE {table_prefix}CB_AUTH_CONFIGURATION; +DROP TABLE {table_prefix}CB_AUTH_PROVIDER; +DROP SEQUENCE IF EXISTS {table_prefix}CB_INSTANCE_EVENT_EVENT_ID; diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_18.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_18.sql new file mode 100644 index 0000000000..bc9592af86 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_18.sql @@ -0,0 +1,16 @@ +CREATE TABLE {table_prefix}CB_USER_PREFERENCES +( + USER_ID VARCHAR(128) NOT NULL, + PREFERENCE_ID VARCHAR(128) NOT NULL, + PREFERENCE_VALUE VARCHAR(1024), + + PRIMARY KEY (USER_ID, PREFERENCE_ID), + FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_USER (USER_ID) ON DELETE CASCADE +); + + +INSERT INTO {table_prefix}CB_USER_PREFERENCES (USER_ID, PREFERENCE_ID, PREFERENCE_VALUE) +SELECT USER_ID, PARAM_ID, PARAM_VALUE FROM {table_prefix}CB_USER_PARAMETERS; + +DROP TABLE {table_prefix}CB_USER_PARAMETERS; + diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_19.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_19.sql new file mode 100644 index 0000000000..5639d46483 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_19.sql @@ -0,0 +1,5 @@ +ALTER TABLE {table_prefix}CB_AUTH_ATTEMPT + ADD ERROR_CODE VARCHAR(128) NULL; + +ALTER TABLE {table_prefix}CB_AUTH_ATTEMPT + ADD FORCE_SESSION_LOGOUT CHAR(1) DEFAULT 'N' NOT NULL; \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_2.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_2.sql index ee14af9683..fbffddcab3 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_2.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_2.sql @@ -55,7 +55,7 @@ CREATE TABLE IF NOT EXISTS {table_prefix}CB_WORKSPACE( FOREIGN KEY(INSTANCE_ID) REFERENCES {table_prefix}CB_INSTANCE(INSTANCE_ID) ); -ALTER TABLE {table_prefix}CB_USER_CREDENTIALS ADD UPDATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE {table_prefix}CB_USER_CREDENTIALS ADD UPDATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; ALTER TABLE {table_prefix}CB_SESSION ALTER COLUMN LAST_ACCESS_REMOTE_ADDRESS VARCHAR(128) NULL; ALTER TABLE {table_prefix}CB_SESSION ALTER COLUMN LAST_ACCESS_USER_AGENT VARCHAR(255) NULL; diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_20.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_20.sql new file mode 100644 index 0000000000..230dfbec43 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_20.sql @@ -0,0 +1,2 @@ +ALTER TABLE {table_prefix}CB_USER_TEAM + ADD TEAM_ROLE VARCHAR(128) NULL; diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_21.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_21.sql new file mode 100644 index 0000000000..fd7739c5b1 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_21.sql @@ -0,0 +1,8 @@ +CREATE TABLE {table_prefix}CB_TASKS +( + TASK_ID VARCHAR(128) NOT NULL, + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + TIMEOUT INTEGER NOT NULL, + + PRIMARY KEY (TASK_ID) +); \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_22.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_22.sql new file mode 100644 index 0000000000..8ac5fdab27 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_22.sql @@ -0,0 +1,12 @@ +CREATE TABLE {table_prefix}CB_ACCESS_TOKEN +( + TOKEN_ID VARCHAR(128) NOT NULL, + USER_ID VARCHAR(128) NOT NULL, + TOKEN_NAME VARCHAR(128) NOT NULL, + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + EXPIRATION_TIME TIMESTAMP NULL, + + PRIMARY KEY (USER_ID, TOKEN_ID), + UNIQUE (USER_ID, TOKEN_NAME), + FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_USER(USER_ID) ON DELETE CASCADE +); \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_23.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_23.sql new file mode 100644 index 0000000000..5bfef3bfca --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_23.sql @@ -0,0 +1,5 @@ +ALTER TABLE {table_prefix}CB_AUTH_ATTEMPT + ADD IS_SERVICE_AUTH CHAR(1) DEFAULT 'N' NOT NULL; + +ALTER TABLE {table_prefix}CB_AUTH_TOKEN + ADD IS_SERVICE CHAR(1) DEFAULT 'N' NOT NULL; \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_24.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_24.sql new file mode 100644 index 0000000000..486d55d3e1 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_24.sql @@ -0,0 +1,2 @@ +ALTER TABLE {table_prefix}CB_SCHEMA_INFO +ADD MODULE_ID VARCHAR(10) DEFAULT 'CB_CE' NOT NULL; \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_25.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_25.sql new file mode 100644 index 0000000000..cb167ade26 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_25.sql @@ -0,0 +1,3 @@ +ALTER TABLE {table_prefix}CB_USER ADD CHANGE_DATE TIMESTAMP NULL; +ALTER TABLE {table_prefix}CB_USER ADD DISABLED_BY VARCHAR(128) NULL; +ALTER TABLE {table_prefix}CB_USER ADD DISABLE_REASON VARCHAR(128) NULL; diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_5.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_5.sql index c15cbf971c..ea46bc1458 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_5.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_5.sql @@ -5,9 +5,9 @@ CREATE TABLE {table_prefix}CB_AUTH_TOKEN USER_ID VARCHAR(128), EXPIRATION_TIME TIMESTAMP NOT NULL, - CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (TOKEN_ID), FOREIGN KEY (SESSION_ID) REFERENCES {table_prefix}CB_SESSION (SESSION_ID) ON DELETE CASCADE, FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_USER (USER_ID) ON DELETE CASCADE -); +); \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql index 410597bc0e..64cb9ca652 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql @@ -8,7 +8,7 @@ CREATE TABLE {table_prefix}CB_AUTH_ATTEMPT SESSION_TYPE VARCHAR(64) NOT NULL, APP_SESSION_STATE TEXT NOT NULL, - CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (AUTH_ID), FOREIGN KEY (SESSION_ID) REFERENCES {table_prefix}CB_SESSION (SESSION_ID) ON DELETE CASCADE @@ -21,7 +21,7 @@ CREATE TABLE {table_prefix}CB_AUTH_ATTEMPT_INFO AUTH_PROVIDER_CONFIGURATION_ID VARCHAR(128), AUTH_STATE TEXT NOT NULL, - CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (AUTH_ID, AUTH_PROVIDER_ID), FOREIGN KEY (AUTH_ID) REFERENCES {table_prefix}CB_AUTH_ATTEMPT (AUTH_ID) ON DELETE CASCADE diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_9.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_9.sql index ca028aabbc..a6c7e3e712 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_9.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_9.sql @@ -2,4 +2,4 @@ ALTER TABLE {table_prefix}CB_AUTH_TOKEN ADD REFRESH_TOKEN_ID VARCHAR(128); ALTER TABLE {table_prefix}CB_AUTH_TOKEN - ADD REFRESH_TOKEN_EXPIRATION_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file + ADD REFRESH_TOKEN_EXPIRATION_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/plugin.xml b/server/bundles/io.cloudbeaver.service.security/plugin.xml index b26bf58784..2fefe21d11 100644 --- a/server/bundles/io.cloudbeaver.service.security/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.security/plugin.xml @@ -4,13 +4,14 @@ + icon="platform:/plugin/org.jkiss.dbeaver.model/icons/tree/key.svg"> - - + + @@ -18,13 +19,26 @@ + icon="platform:/plugin/org.jkiss.dbeaver.model/icons/tree/key.svg"> + + + + + + + + + + + + diff --git a/server/bundles/io.cloudbeaver.service.security/pom.xml b/server/bundles/io.cloudbeaver.service.security/pom.xml index d921c7c4a5..498e1d3ff2 100644 --- a/server/bundles/io.cloudbeaver.service.security/pom.xml +++ b/server/bundles/io.cloudbeaver.service.security/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.security - 1.0.38-SNAPSHOT + 1.0.85-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java index 9559f513b4..60cf8809cc 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,6 +16,7 @@ */ package io.cloudbeaver.auth.provider.local; +import io.cloudbeaver.auth.SMBruteForceProtected; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; @@ -35,7 +36,7 @@ /** * Local auth provider */ -public class LocalAuthProvider implements SMAuthProvider { +public class LocalAuthProvider implements SMAuthProvider, SMBruteForceProtected { public static final String PROVIDER_ID = LocalAuthProviderConstants.PROVIDER_ID; public static final String CRED_USER = LocalAuthProviderConstants.CRED_USER; @@ -67,7 +68,9 @@ public String validateLocalAuth(@NotNull DBRProgressMonitor monitor, throw new DBException("No user password provided"); } String clientPasswordHash = AuthPropertyEncryption.hash.encrypt(userName, clientPassword); - if (!storedPasswordHash.equals(clientPasswordHash)) { + // we also need to check a hash with lower case (CB-5833) + String clientPasswordHashLowerCase = AuthPropertyEncryption.hash.encrypt(userName.toLowerCase(), clientPassword); + if (!storedPasswordHash.equals(clientPasswordHash) && !clientPasswordHashLowerCase.equals(storedPasswordHash)) { throw new DBException("Invalid user name or password"); } @@ -126,4 +129,8 @@ public static boolean changeUserPassword(@NotNull WebSession webSession, @NotNul return true; } + @Override + public Object getInputUsername(@NotNull Map cred) { + return cred.get("user"); + } } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthSession.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthSession.java index cd000b8cde..319b061065 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthSession.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthSession.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,6 +17,7 @@ package io.cloudbeaver.auth.provider.local; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.auth.SMAuthSpace; import org.jkiss.dbeaver.model.auth.SMSession; import org.jkiss.dbeaver.model.auth.SMSessionContext; @@ -54,6 +55,7 @@ public SMSessionContext getSessionContext() { return webSession.getSessionContext(); } + @Nullable @Override public SMSessionPrincipal getSessionPrincipal() { return webSession.getSessionPrincipal(); diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/rp/RPAuthProvider.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/rp/RPAuthProvider.java index 7f799aca2d..dd62826fa0 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/rp/RPAuthProvider.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/rp/RPAuthProvider.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -18,6 +18,7 @@ import io.cloudbeaver.DBWUserIdentity; import io.cloudbeaver.auth.SMAuthProviderExternal; +import io.cloudbeaver.auth.SMSignOutLinkProvider; import io.cloudbeaver.auth.provider.local.LocalAuthSession; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.user.WebUser; @@ -37,15 +38,20 @@ import java.util.HashMap; import java.util.Map; -public class RPAuthProvider implements SMAuthProviderExternal { +public class RPAuthProvider implements SMAuthProviderExternal, SMSignOutLinkProvider { private static final Log log = Log.getLog(RPAuthProvider.class); public static final String X_USER = "X-User"; + @Deprecated // use X-Team public static final String X_ROLE = "X-Role"; + public static final String X_TEAM = "X-Team"; + public static final String X_ROLE_TE = "X-Role-TE"; public static final String X_FIRST_NAME = "X-First-name"; public static final String X_LAST_NAME = "X-Last-name"; + public static final String X_FULL_NAME = "X-Full-name"; public static final String AUTH_PROVIDER = "reverseProxy"; + public static final String LOGOUT_URL = "logout-url"; @NotNull @Override @@ -77,6 +83,7 @@ public DBWUserIdentity getUserIdentity( Map userMeta = new HashMap<>(); String firstName = JSONUtils.getString(authParameters, SMStandardMeta.META_FIRST_NAME); String lastName = JSONUtils.getString(authParameters, SMStandardMeta.META_LAST_NAME); + String fullName = JSONUtils.getString(authParameters, "fullName"); if (CommonUtils.isNotEmpty(firstName)) { nameBuilder.append(firstName); userMeta.put(SMStandardMeta.META_FIRST_NAME, firstName); @@ -87,6 +94,10 @@ public DBWUserIdentity getUserIdentity( userMeta.put(SMStandardMeta.META_LAST_NAME, lastName); } + if (CommonUtils.isNotEmpty(fullName)) { + nameBuilder = new StringBuilder(fullName); + } + return new DBWUserIdentity( userName, nameBuilder.length() > 0 ? nameBuilder.toString() : userName, @@ -119,4 +130,26 @@ public void closeSession(@NotNull SMSession mainSession, SMSession session) thro public void refreshSession(@NotNull DBRProgressMonitor monitor, @NotNull SMSession mainSession, SMSession session) throws DBException { } + + @NotNull + @Override + public String getCommonSignOutLink( + @NotNull String providerId, + @NotNull Map providerConfig, + @NotNull String origin + ) throws DBException { + return providerConfig.get(LOGOUT_URL) != null ? providerConfig.get(LOGOUT_URL).toString() : ""; + } + + @Override + public String getUserSignOutLink( + @NotNull SMAuthProviderCustomConfiguration providerConfig, + @NotNull Map userCredentials, + @NotNull String origin + ) throws DBException { + return providerConfig.getParameters().get(LOGOUT_URL) != null ? + providerConfig.getParameters().get(LOGOUT_URL).toString() : + null; + } + } 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 875de6b57d..13b8592fb1 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -19,26 +19,29 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; -import io.cloudbeaver.auth.SMAuthProviderAssigner; -import io.cloudbeaver.auth.SMAuthProviderExternal; -import io.cloudbeaver.auth.SMAuthProviderFederated; -import io.cloudbeaver.auth.SMAutoAssign; -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.DBWConstants; +import io.cloudbeaver.auth.*; +import io.cloudbeaver.model.app.ServletAuthApplication; +import io.cloudbeaver.model.app.ServletAuthConfiguration; +import io.cloudbeaver.model.config.SMControllerConfiguration; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; import io.cloudbeaver.registry.WebMetaParametersRegistry; +import io.cloudbeaver.service.security.bruteforce.BruteForceUtils; +import io.cloudbeaver.service.security.bruteforce.UserLoginRecord; import io.cloudbeaver.service.security.db.CBDatabase; import io.cloudbeaver.service.security.internal.AuthAttemptSessionInfo; +import io.cloudbeaver.service.security.internal.CBAuthSubjectRepo; import io.cloudbeaver.service.security.internal.SMTokenInfo; +import io.cloudbeaver.utils.WebEventUtils; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPConnectionInformation; import org.jkiss.dbeaver.model.DBPPage; import org.jkiss.dbeaver.model.auth.*; +import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.impl.jdbc.JDBCUtils; import org.jkiss.dbeaver.model.impl.jdbc.exec.JDBCTransaction; @@ -51,13 +54,17 @@ import org.jkiss.dbeaver.model.security.exception.SMRefreshTokenExpiredException; import org.jkiss.dbeaver.model.security.user.*; import org.jkiss.dbeaver.model.sql.SQLUtils; +import org.jkiss.dbeaver.model.websocket.event.WSUserCloseSessionsEvent; +import org.jkiss.dbeaver.model.websocket.event.WSUserDeletedEvent; +import org.jkiss.dbeaver.model.websocket.event.WSUserDisabledEvent; import org.jkiss.dbeaver.model.websocket.event.permissions.WSObjectPermissionEvent; -import org.jkiss.dbeaver.model.websocket.event.permissions.WSSubjectPermissionEvent; +import org.jkiss.dbeaver.model.websocket.event.session.WSAuthEvent; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.SecurityUtils; import java.lang.reflect.Type; +import java.net.URI; import java.sql.*; import java.time.Instant; import java.time.LocalDateTime; @@ -67,27 +74,26 @@ /** * Server controller */ -public class CBEmbeddedSecurityController implements SMAdminController, SMAuthenticationManager { +public class CBEmbeddedSecurityController + implements SMAdminController, SMAuthenticationManager { private static final Log log = Log.getLog(CBEmbeddedSecurityController.class); protected static final String CHAR_BOOL_TRUE = "Y"; protected static final String CHAR_BOOL_FALSE = "N"; - private static final String SUBJECT_USER = "U"; - private static final String SUBJECT_TEAM = "R"; private static final Type MAP_STRING_OBJECT_TYPE = new TypeToken>() { }.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 @@ -98,10 +104,10 @@ public CBEmbeddedSecurityController( this.smConfig = smConfig; } - private boolean isSubjectExists(String subjectId) throws DBCException { + protected boolean isSubjectExists(String subjectId) throws DBCException { try (Connection dbCon = database.openConnection()) { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT 1 FROM {table_prefix}CB_AUTH_SUBJECT WHERE SUBJECT_ID=?")) + "SELECT 1 FROM {table_prefix}CB_AUTH_SUBJECT WHERE SUBJECT_ID=?") ) { dbStat.setString(1, subjectId); try (ResultSet dbResult = dbStat.executeQuery()) { @@ -116,12 +122,24 @@ private boolean isSubjectExists(String subjectId) throws DBCException { /////////////////////////////////////////// // Users + /** + * Creates user. Saves user id in database as provided. + */ @Override public void createUser( @NotNull String userId, @Nullable Map metaParameters, boolean enabled, @Nullable String defaultAuthRole + ) throws DBException { + validateAndCreateUser(userId, metaParameters, enabled, defaultAuthRole); + } + + protected void validateAndCreateUser( + @NotNull String userId, + @Nullable Map metaParameters, + boolean enabled, + @Nullable String defaultAuthRole ) throws DBException { if (CommonUtils.isEmpty(userId)) { throw new DBCException("Empty user name is not allowed"); @@ -132,41 +150,73 @@ 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(); - } - saveSubjectMetas(dbCon, userId, metaParameters); + insertUser(dbCon, userId, metaParameters, enabled, defaultAuthRole); 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); } } + /** + * Creates user. Saves user id in database as it is. + */ + protected void insertUser( + @NotNull Connection dbCon, + @NotNull String userId, + @Nullable Map metaParameters, + boolean enabled, + @Nullable String defaultAuthRole + ) throws DBException, SQLException { + createAuthSubject(dbCon, userId, SMSubjectType.user, true); + try (PreparedStatement dbStat = dbCon.prepareStatement( + "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(); + log.info(String.format("New user created: [userId=%s]", userId)); + } + saveSubjectMetas(dbCon, userId, metaParameters); + String defaultTeamName = getDefaultUserTeam(); + if (!CommonUtils.isEmpty(defaultTeamName)) { + setUserTeams(dbCon, userId, new String[]{defaultTeamName}, userId); + } + } + @Override public void importUsers(@NotNull SMUserImportList userImportList) throws DBException { + try (var dbCon = database.openConnection()) { + importUsers(dbCon, userImportList); + } catch (SQLException e) { + log.error("Failed attempt import user: " + e.getMessage()); + } + } + + protected void importUsers(@NotNull Connection connection, @NotNull SMUserImportList userImportList) + throws DBException, SQLException { + outer: for (SMUserProvisioning user : userImportList.getUsers()) { - if (isSubjectExists(user.getUserId())) { - log.info("Skip already exist user: " + user.getUserId()); + String authRole = user.getAuthRole() == null ? userImportList.getAuthRole() : user.getAuthRole(); + String userId = user.getUserId(); + Map metaParameters = user.getMetaParameters(); + if (CommonUtils.isNotEmpty(metaParameters.get(SMStandardMeta.META_USER_ID))) { + userId = metaParameters.get(SMStandardMeta.META_USER_ID); + } + if (isSubjectExists(userId)) { + log.info("User already exist : " + userId); + setUserAuthRole(connection, userId, authRole); + enableUser(connection, userId, true, null, null); continue; } - createUser(user.getUserId(), user.getMetaParameters(), true, userImportList.getAuthRole()); + insertUser(connection, userId, metaParameters, true, authRole); } } @@ -178,7 +228,7 @@ public void deleteUser(String userId) throws DBCException { deleteAuthSubject(dbCon, userId); JDBCUtils.executeStatement( dbCon, - database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER WHERE USER_ID=?"), + "DELETE FROM {table_prefix}CB_USER WHERE USER_ID=?", userId ); txn.commit(); @@ -186,31 +236,27 @@ public void deleteUser(String userId) throws DBCException { } catch (SQLException e) { throw new DBCException("Error deleting user from database", e); } + var event = new WSUserDeletedEvent(userId); + application.getEventController().addEvent(event); + log.info(String.format("User deleted: [userId=%s]", userId)); } - @Override 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) { + throw new DBCException("Error saving user teams in database", e); + } + addSubjectPermissionsUpdateEvent(userId, SMSubjectType.user); + } + + public void addUserTeams(@NotNull String userId, @NotNull String[] teamIds, @NotNull String grantorId) throws DBCException { + try (Connection dbCon = database.openConnection()) { + try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { + addUserTeams(dbCon, userId, teamIds, grantorId); txn.commit(); } } catch (SQLException e) { @@ -219,27 +265,198 @@ public void setUserTeams(String userId, String[] teamIds, String grantorId) thro addSubjectPermissionsUpdateEvent(userId, SMSubjectType.user); } + public void deleteUserTeams(@NotNull String userId, @NotNull String[] teamIds) throws DBCException { + try (Connection dbCon = database.openConnection()) { + try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { + deleteUserTeams(dbCon, userId, teamIds); + txn.commit(); + } + } catch (SQLException e) { + throw new DBCException("Error delete user teams in database", e); + } + addSubjectPermissionsUpdateEvent(userId, SMSubjectType.user); + } + + @Override + public void setUserTeamRole( + @NotNull String userId, + @NotNull String teamId, + @Nullable String teamRole + ) throws DBException { + if (!isSubjectExists(userId)) { + throw new DBCException("User '" + userId + "' doesn't exists"); + } + if (!isSubjectExists(teamId)) { + throw new DBCException("Team '" + teamId + "' doesn't exists"); + } + + try ( + var dbCon = database.openConnection(); + PreparedStatement dbStat = dbCon.prepareStatement( + "UPDATE {table_prefix}CB_USER_TEAM " + + "SET TEAM_ROLE=? WHERE USER_ID=? AND TEAM_ID=?") + ) { + JDBCUtils.setStringOrNull(dbStat, 1, teamRole); + dbStat.setString(2, userId); + dbStat.setString(3, teamId); + dbStat.execute(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + log.info(String.format( + "User set team role: [userId=%s,teamId=%s, role=%s]", + userId, + teamId, + teamRole + )); + } + + //TODO implement add/delete user teams api + protected void setUserTeams(@NotNull Connection dbCon, String userId, String[] teamIds, String grantorId) + throws SQLException { + + String deleteUserTeamsSql = "DELETE FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?"; + if (!ArrayUtils.isEmpty(teamIds)) { + deleteUserTeamsSql = + deleteUserTeamsSql + " AND TEAM_ID NOT IN (" + SQLUtils.generateParamList(teamIds.length) + ")"; + } + try (PreparedStatement dbStat = dbCon.prepareStatement(deleteUserTeamsSql)) { + int index = 1; + dbStat.setString(index++, userId); + for (String teamId : teamIds) { + dbStat.setString(index++, teamId); + } + dbStat.execute(); + } + + String defaultUserTeam = getDefaultUserTeam(); + if (CommonUtils.isNotEmpty(defaultUserTeam) && !ArrayUtils.contains(teamIds, defaultUserTeam)) { + teamIds = ArrayUtils.add(String.class, teamIds, defaultUserTeam); + } + if (!ArrayUtils.isEmpty(teamIds)) { + Set currentUserTeams = getCurrentUserTeams(dbCon, userId); + + try (PreparedStatement dbStat = dbCon.prepareStatement( + "INSERT INTO {table_prefix}CB_USER_TEAM" + + "(USER_ID,TEAM_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)") + ) { + for (String teamId : teamIds) { + if (currentUserTeams.contains(teamId)) { + continue; + } + dbStat.setString(1, userId); + dbStat.setString(2, teamId); + dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); + dbStat.setString(4, grantorId); + dbStat.execute(); + } + } + } + } + + @NotNull + private Set getCurrentUserTeams(@NotNull Connection dbCon, String userId) throws SQLException { + return new HashSet<>(JDBCUtils.queryStrings( + dbCon, + "SELECT TEAM_ID FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?", + userId + )); + } + + protected void addUserTeams( + @NotNull Connection dbCon, + @NotNull String userId, + @NotNull String[] teamIds, + @NotNull String grantorId + ) throws SQLException { + if (ArrayUtils.isEmpty(teamIds)) { + return; + } + + String defaultUserTeam = getDefaultUserTeam(); + if (CommonUtils.isNotEmpty(defaultUserTeam) && !ArrayUtils.contains(teamIds, defaultUserTeam)) { + teamIds = ArrayUtils.add(String.class, teamIds, defaultUserTeam); + } + + Set currentUserTeams = getCurrentUserTeams(dbCon, userId); + + List resultTeamIds = new ArrayList<>(); + try (PreparedStatement dbStat = dbCon.prepareStatement( + "INSERT INTO {table_prefix}CB_USER_TEAM" + + "(USER_ID,TEAM_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)") + ) { + for (String teamId : teamIds) { + if (currentUserTeams.contains(teamId)) { + continue; + } + resultTeamIds.add(teamId); + dbStat.setString(1, userId); + dbStat.setString(2, teamId); + dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); + dbStat.setString(4, grantorId); + dbStat.execute(); + } + } + log.info(String.format( + "User added to team: [userId=%s,team=%s, grantorUserId=%s]", + userId, + String.join(",", resultTeamIds), + grantorId + )); + } + + protected void deleteUserTeams( + @NotNull Connection dbCon, + @NotNull String userId, + @NotNull String[] teamIds + ) throws SQLException, DBCException { + String defaultTeam = getDefaultUserTeam(); + if (ArrayUtils.contains(teamIds, defaultTeam)) { + throw new SMException("Cannot delete default user team: " + defaultTeam); + } + if (ArrayUtils.isEmpty(teamIds)) { + return; + } + String deleteUserTeamsSql = "DELETE FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=? " + + "AND TEAM_ID IN (" + SQLUtils.generateParamList(teamIds.length) + ")"; + + try (PreparedStatement dbStat = dbCon.prepareStatement(deleteUserTeamsSql)) { + int index = 1; + dbStat.setString(index, userId); + for (String teamId : teamIds) { + index++; + dbStat.setString(index, teamId); + } + dbStat.execute(); + } + log.info(String.format("User deleted from team: [userId=%s,teamIds=%s]", userId, String.join(",", teamIds))); + } @NotNull @Override - public SMTeam[] getUserTeams(String userId) throws DBException { - Map teams = new LinkedHashMap<>(); + public SMUserTeam[] getUserTeams(String userId) throws DBException { + Map teams = new LinkedHashMap<>(); try (Connection dbCon = database.openConnection()) { + String defaultUserTeam = getDefaultUserTeam(); try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT R.* FROM {table_prefix}CB_USER_TEAM UR, {table_prefix}CB_TEAM R " + - "WHERE UR.USER_ID=? AND UR.TEAM_ID=R.TEAM_ID")) + "SELECT R.*,S.IS_SECRET_STORAGE,UR.TEAM_ROLE FROM {table_prefix}CB_USER_TEAM UR, {table_prefix}CB_TEAM R, " + + "{table_prefix}CB_AUTH_SUBJECT S " + + "WHERE UR.USER_ID=? AND UR.TEAM_ID = R.TEAM_ID " + + "AND S.SUBJECT_ID IN (R.TEAM_ID,?)") ) { dbStat.setString(1, userId); + dbStat.setString(2, defaultUserTeam); try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { var team = fetchTeam(dbResult); - teams.put(team.getTeamId(), team); + String teamRole = dbResult.getString("TEAM_ROLE"); + teams.put(team.getTeamId(), new SMUserTeam(team, teamRole)); } } } readSubjectsMetas(dbCon, SMSubjectType.team, null, teams); - return teams.values().toArray(new SMTeam[0]); + return teams.values().toArray(new SMUserTeam[0]); } catch (SQLException e) { throw new DBCException("Error while reading user teams", e); } @@ -249,7 +466,7 @@ private Set getAllLinkedSubjects(Connection dbCon, String subjectId) thr Set allSubjects = new HashSet<>(); allSubjects.add(subjectId); try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT TEAM_ID FROM {table_prefix}CB_USER_TEAM UR WHERE USER_ID=?")) + "SELECT TEAM_ID FROM {table_prefix}CB_USER_TEAM UR WHERE USER_ID=?") ) { dbStat.setString(1, subjectId); try (ResultSet dbResult = dbStat.executeQuery()) { @@ -263,7 +480,7 @@ private Set getAllLinkedSubjects(Connection dbCon, String subjectId) thr @NotNull @Override - public SMTeam[] getCurrentUserTeams() throws DBException { + public SMUserTeam[] getCurrentUserTeams() throws DBException { return getUserTeams(getUserIdOrThrow()); } @@ -272,15 +489,15 @@ public SMUser getUserById(String userId) throws DBException { try (Connection dbCon = database.openConnection()) { SMUser user; try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT USER_ID,IS_ACTIVE,DEFAULT_AUTH_ROLE FROM {table_prefix}CB_USER WHERE USER_ID=?") - )) { + """ + SELECT U.USER_ID,U.IS_ACTIVE,U.DEFAULT_AUTH_ROLE,S.IS_SECRET_STORAGE,U.CHANGE_DATE,U.DISABLED_BY,U.DISABLE_REASON + FROM {table_prefix}CB_USER U, {table_prefix}CB_AUTH_SUBJECT S + WHERE U.USER_ID=? AND U.USER_ID=S.SUBJECT_ID""") + ) { dbStat.setString(1, userId); try (ResultSet dbResult = dbStat.executeQuery()) { if (dbResult.next()) { - String userName = dbResult.getString(1); - String active = dbResult.getString(2); - String authRole = dbResult.getString(3); - user = new SMUser(userName, CHAR_BOOL_TRUE.equals(active), authRole); + user = fetchUser(dbResult, true); } else { return null; } @@ -288,15 +505,16 @@ public SMUser getUserById(String userId) throws DBException { } readSubjectMetas(dbCon, user); // Teams - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT TEAM_ID FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?")) + try (PreparedStatement dbStat = dbCon.prepareStatement("SELECT TEAM_ID FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?") ) { + String defaultUserTeam = getDefaultUserTeam(); dbStat.setString(1, userId); try (ResultSet dbResult = dbStat.executeQuery()) { - List teamIDs = new ArrayList<>(); + Set teamIDs = new LinkedHashSet<>(); while (dbResult.next()) { teamIDs.add(dbResult.getString(1)); } + teamIDs.add(defaultUserTeam); user.setUserTeams(teamIDs.toArray(new String[0])); } } @@ -315,8 +533,8 @@ public SMUser getCurrentUser() throws DBException { @Override public int countUsers(@NotNull SMUserFilter filter) throws DBCException { try (Connection dbCon = database.openConnection()) { - try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames( - "SELECT COUNT(*) FROM {table_prefix}CB_USER" + buildUsersFilter(filter)))) { + try (PreparedStatement dbStat = dbCon.prepareStatement( + "SELECT COUNT(*) FROM {table_prefix}CB_USER" + buildUsersFilter(filter))) { setUsersFilterValues(dbStat, filter, 1); try (ResultSet dbResult = dbStat.executeQuery()) { if (dbResult.next()) { @@ -345,18 +563,15 @@ public SMUser[] findUsers(@NotNull SMUserFilter filter) Map result = new LinkedHashMap<>(); // Read users try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT USER_ID,IS_ACTIVE,DEFAULT_AUTH_ROLE FROM {table_prefix}CB_USER" - + buildUsersFilter(filter) + "\nORDER BY USER_ID LIMIT ? OFFSET ?"))) { - int parameterIndex = setUsersFilterValues(dbStat, filter, 1); - dbStat.setInt(parameterIndex++, filter.getPage().getLimit()); - dbStat.setInt(parameterIndex++, filter.getPage().getOffset()); + "SELECT USER_ID,IS_ACTIVE,DEFAULT_AUTH_ROLE,CHANGE_DATE,DISABLED_BY," + + "DISABLE_REASON FROM {table_prefix}CB_USER" + + buildUsersFilter(filter) + "\nORDER BY USER_ID " + getOffsetLimitPart(filter))) { + setUsersFilterValues(dbStat, filter, 1); try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { - String userId = dbResult.getString(1); - String active = dbResult.getString(2); - String authRole = dbResult.getString(3); - result.put(userId, new SMUser(userId, CHAR_BOOL_TRUE.equals(active), authRole)); + SMUser user = fetchUser(dbResult, false); + result.put(user.getUserId(), user); } } } @@ -365,14 +580,11 @@ public SMUser[] findUsers(@NotNull SMUserFilter filter) } readSubjectsMetas(dbCon, SMSubjectType.user, filter.getUserIdMask(), result); - StringBuilder teamsSql = new StringBuilder() - .append("SELECT USER_ID,TEAM_ID FROM {table_prefix}CB_USER_TEAM") - .append("\n") - .append("WHERE USER_ID IN (") - .append(SQLUtils.generateParamList(result.size())) - .append(")"); + String teamsSql = + "SELECT USER_ID,TEAM_ID FROM {table_prefix}CB_USER_TEAM\n" + + "WHERE USER_ID IN (" + SQLUtils.generateParamList(result.size()) + ")"; // Read teams - try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(teamsSql.toString()))) { + try (PreparedStatement dbStat = dbCon.prepareStatement(teamsSql)) { int parameterIndex = 1; for (String userId : result.keySet()) { dbStat.setString(parameterIndex++, userId); @@ -395,6 +607,10 @@ public SMUser[] findUsers(@NotNull SMUserFilter filter) } } + private String getOffsetLimitPart(@NotNull SMUserFilter filter) { + return database.getDialect().getOffsetLimitQueryPart(filter.getPage().getOffset(), filter.getPage().getLimit()); + } + private String buildUsersFilter(SMUserFilter filter) { StringBuilder where = new StringBuilder(); List whereParts = new ArrayList<>(); @@ -404,7 +620,7 @@ private String buildUsersFilter(SMUserFilter filter) { if (filter.getEnabledState() != null) { whereParts.add("IS_ACTIVE=?"); } - if (whereParts.size() > 0) { + if (!whereParts.isEmpty()) { where.append(whereParts.stream().collect(Collectors.joining(" AND ", " WHERE ", ""))); } return where.toString(); @@ -425,7 +641,7 @@ private int setUsersFilterValues(PreparedStatement dbStat, SMUserFilter filter, private void cleanupSubjectMeta(Connection dbCon, String subjectId) throws SQLException { // Delete old metas try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("DELETE FROM {table_prefix}CB_SUBJECT_META WHERE SUBJECT_ID=?")) + "DELETE FROM {table_prefix}CB_SUBJECT_META WHERE SUBJECT_ID=?") ) { dbStat.setString(1, subjectId); dbStat.execute(); @@ -435,7 +651,7 @@ private void cleanupSubjectMeta(Connection dbCon, String subjectId) throws SQLEx private void readSubjectMetas(Connection dbCon, SMSubject subject) throws SQLException { // Metas try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT META_ID,META_VALUE FROM {table_prefix}CB_SUBJECT_META WHERE SUBJECT_ID=?")) + "SELECT META_ID,META_VALUE FROM {table_prefix}CB_SUBJECT_META WHERE SUBJECT_ID=?") ) { dbStat.setString(1, subject.getSubjectId()); try (ResultSet dbResult = dbStat.executeQuery()) { @@ -450,13 +666,13 @@ private void readSubjectMetas(Connection dbCon, SMSubject subject) throws SQLExc } private void readSubjectsMetas(Connection dbCon, SMSubjectType subjectType, String userIdMask, - Map result) throws SQLException { + Map result) throws SQLException { // Read metas try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT m.SUBJECT_ID,m.META_ID,m.META_VALUE FROM {table_prefix}CB_AUTH_SUBJECT s, " + + "SELECT m.SUBJECT_ID,m.META_ID,m.META_VALUE FROM {table_prefix}CB_AUTH_SUBJECT s, " + "{table_prefix}CB_SUBJECT_META m\n" + "WHERE s.SUBJECT_TYPE=? AND s.SUBJECT_ID=m.SUBJECT_ID" + - (CommonUtils.isEmpty(userIdMask) ? "" : " AND s.SUBJECT_ID LIKE ?"))) + (CommonUtils.isEmpty(userIdMask) ? "" : " AND s.SUBJECT_ID LIKE ?")) ) { dbStat.setString(1, subjectType.getCode()); if (!CommonUtils.isEmpty(userIdMask)) { @@ -480,7 +696,7 @@ private void readSubjectsMetas(Connection dbCon, SMSubjectType subjectType, Stri private void saveSubjectMetas(Connection dbCon, String subjectId, Map metaParameters) throws SQLException { if (!CommonUtils.isEmpty(metaParameters)) { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_SUBJECT_META(SUBJECT_ID,META_ID,META_VALUE) VALUES(?,?,?)")) + "INSERT INTO {table_prefix}CB_SUBJECT_META(SUBJECT_ID,META_ID,META_VALUE) VALUES(?,?,?)") ) { dbStat.setString(1, subjectId); for (Map.Entry mp : metaParameters.entrySet()) { @@ -499,7 +715,7 @@ public Map getCurrentUserParameters() throws DBCException { Map result = new LinkedHashMap<>(); // Read users try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT * FROM {table_prefix}CB_USER_PARAMETERS WHERE USER_ID=?")) + "SELECT * FROM {table_prefix}CB_USER_PREFERENCES WHERE USER_ID=?") ) { dbStat.setString(1, userId); try (ResultSet dbResult = dbStat.executeQuery()) { @@ -517,63 +733,105 @@ public Map getCurrentUserParameters() throws DBCException { } @Override - public void setCurrentUserParameter(String name, Object value) throws DBException { + public void setCurrentUserParameter(@NotNull String name, @Nullable Object value) throws DBException { String userId = getUserIdOrThrow(); - try (Connection dbCon = database.openConnection()) { - try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { - if (value == null) { - // Delete old metas - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER_PARAMETERS WHERE USER_ID=? AND PARAM_ID=?")) - ) { - dbStat.setString(1, userId); - dbStat.setString(2, name); - dbStat.execute(); - } - } else { - // Update/Insert parameter - boolean updated; - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("UPDATE {table_prefix}CB_USER_PARAMETERS " + - "SET PARAM_VALUE=? WHERE USER_ID=? AND PARAM_ID=?")) - ) { - dbStat.setString(1, CommonUtils.toString(value)); - dbStat.setString(2, userId); - dbStat.setString(3, name); - updated = dbStat.executeUpdate() > 0; - } - if (!updated) { - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER_PARAMETERS " + - "(USER_ID,PARAM_ID,PARAM_VALUE) VALUES(?,?,?)")) - ) { - dbStat.setString(1, userId); - dbStat.setString(2, name); - dbStat.setString(3, CommonUtils.toString(value)); - dbStat.executeUpdate(); - } - } - } - txn.commit(); - } + try (Connection dbCon = database.openConnection(); + JDBCTransaction txn = new JDBCTransaction(dbCon) + ) { + updateUserParameterValue(dbCon, userId, name, value); + txn.commit(); } catch (SQLException e) { throw new DBCException("Error while updating user configuration", e); } } - public void enableUser(String userId, boolean enabled) throws DBException { - try (Connection dbCon = database.openConnection()) { + private void updateUserParameterValue( + @NotNull Connection dbCon, @NotNull String userId, @NotNull String name, @Nullable Object value + ) throws SQLException { + if (value == null) { + // Delete old metas + try (PreparedStatement dbStat = dbCon.prepareStatement( + "DELETE FROM {table_prefix}CB_USER_PREFERENCES WHERE USER_ID=? AND PREFERENCE_ID=?") + ) { + dbStat.setString(1, userId); + dbStat.setString(2, name); + dbStat.execute(); + } + } else { + // Update/Insert parameter + boolean updated; 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); + "UPDATE {table_prefix}CB_USER_PREFERENCES SET PREFERENCE_VALUE=? WHERE USER_ID=? AND PREFERENCE_ID=?") + ) { + dbStat.setString(1, CommonUtils.toString(value)); dbStat.setString(2, userId); - dbStat.executeUpdate(); + dbStat.setString(3, name); + updated = dbStat.executeUpdate() > 0; } + if (!updated) { + try (PreparedStatement dbStat = dbCon.prepareStatement( + "INSERT INTO {table_prefix}CB_USER_PREFERENCES (USER_ID,PREFERENCE_ID,PREFERENCE_VALUE) VALUES(?,?,?)") + ) { + dbStat.setString(1, userId); + dbStat.setString(2, name); + dbStat.setString(3, CommonUtils.toString(value)); + dbStat.executeUpdate(); + } + } + } + } + + @Override + public void setCurrentUserParameters(@NotNull Map parameters) throws DBException { + String userId = getUserIdOrThrow(); + try (Connection dbCon = database.openConnection(); + JDBCTransaction txn = new JDBCTransaction(dbCon) + ) { + for (Map.Entry parameter : parameters.entrySet()) { + updateUserParameterValue(dbCon, userId, parameter.getKey(), parameter.getValue()); + } + txn.commit(); + } catch (SQLException e) { + throw new DBCException("Error while updating user configuration", e); + } + } + + public void enableUser( + @NotNull String userId, + boolean enabled, + @Nullable String disabledBy, + @Nullable String disableReason + ) throws DBException { + try (Connection dbCon = database.openConnection()) { + enableUser(dbCon, userId, enabled, disabledBy, disableReason); } catch (SQLException e) { throw new DBCException("Error while updating user configuration", e); } } + protected void enableUser( + @NotNull Connection dbCon, + @NotNull String userId, + boolean enabled, + @Nullable String disabledBy, + @Nullable String disableReason + ) throws SQLException { + try (PreparedStatement dbStat = dbCon.prepareStatement( + "UPDATE {table_prefix}CB_USER SET IS_ACTIVE=?, CHANGE_DATE=?, DISABLED_BY=?, DISABLE_REASON=? WHERE USER_ID=?")) { + dbStat.setString(1, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); + dbStat.setTimestamp(2, new Timestamp(System.currentTimeMillis())); + JDBCUtils.setStringOrNull(dbStat, 3, enabled ? null : disabledBy); + JDBCUtils.setStringOrNull(dbStat, 4, enabled ? null : disableReason); + dbStat.setString(5, userId); + dbStat.executeUpdate(); + } + if (!enabled) { + var event = new WSUserDisabledEvent(userId); + application.getEventController().addEvent(event); + } + log.info(String.format("User updated: [userId=%s, isActive=%s, reason=%s]", userId, enabled, disableReason)); + } + @Override public void setUserAuthRole(@NotNull String userId, @Nullable String authRole) throws DBException { if (credentialsProvider.getActiveUserCredentials() != null @@ -582,20 +840,29 @@ 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( + "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"); + } + log.info(String.format("User set auth role: [userId=%s,role=%s]", userId, authRole)); + } + } /////////////////////////////////////////// @@ -637,14 +904,19 @@ public void setUserCredentials( @NotNull String authProviderId, @NotNull Map credentials ) throws DBException { - var existUserByCredentials = findUserByCredentials(getAuthProvider(authProviderId), credentials); + var existUserByCredentials = findUserByCredentials(getAuthProvider(authProviderId), credentials, false); if (existUserByCredentials != null && !existUserByCredentials.equals(userId)) { throw new DBException("Another user is already linked to the specified credentials"); } List transformedCredentials; WebAuthProviderDescriptor authProvider = getAuthProvider(authProviderId); + if (authProvider.isCaseInsensitive() && !isSubjectExists(userId) && isSubjectExists(userId.toLowerCase())) { + log.warn("User with id '" + userId + "' not found, credentials will be set for the user: " + userId.toLowerCase()); + userId = userId.toLowerCase(); + } try { SMAuthCredentialsProfile credProfile = getCredentialProfileByParameters(authProvider, credentials.keySet()); + String finalUserId = userId; transformedCredentials = credentials.entrySet().stream().map(cred -> { String propertyName = cred.getKey(); AuthPropertyDescriptor property = credProfile.getCredentialParameter(propertyName); @@ -652,9 +924,9 @@ public void setUserCredentials( return null; } String encodedValue = CommonUtils.toString(cred.getValue()); - encodedValue = property.getEncryption().encrypt(userId, encodedValue); + encodedValue = property.getEncryption().encrypt(finalUserId, encodedValue); return new String[]{propertyName, encodedValue}; - }).collect(Collectors.toList()); + }).toList(); } catch (Exception e) { throw new DBCException(e.getMessage(), e); } @@ -662,15 +934,15 @@ public void setUserCredentials( try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { JDBCUtils.executeStatement( dbCon, - database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER_CREDENTIALS WHERE USER_ID=? AND PROVIDER_ID=?"), + "DELETE FROM {table_prefix}CB_USER_CREDENTIALS WHERE USER_ID=? AND PROVIDER_ID=?", userId, authProvider.getId() ); if (!CommonUtils.isEmpty(credentials)) { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER_CREDENTIALS" + + "INSERT INTO {table_prefix}CB_USER_CREDENTIALS" + "(USER_ID,PROVIDER_ID,CRED_ID,CRED_VALUE) VALUES(?,?,?,?)") - )) { + ) { for (String[] cred : transformedCredentials) { if (cred == null) { continue; @@ -688,6 +960,7 @@ public void setUserCredentials( } catch (SQLException e) { throw new DBCException("Error saving user credentials in database", e); } + log.info(String.format("Set credentials for user: [userId=%s,providerId=%s]", userId, authProviderId)); } @Override @@ -695,30 +968,53 @@ public void deleteUserCredentials(@NotNull String userId, @NotNull String authPr try (Connection dbCon = database.openConnection()) { JDBCUtils.executeStatement( dbCon, - database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER_CREDENTIALS WHERE USER_ID=? AND PROVIDER_ID=?"), + "DELETE FROM {table_prefix}CB_USER_CREDENTIALS WHERE USER_ID=? AND PROVIDER_ID=?", userId, authProviderId ); } catch (SQLException e) { throw new DBCException("Error deleting user credentials", e); } + log.info(String.format("User credentials deleted: [userId=%s, providerId=%s]", userId, authProviderId)); + } + + @Nullable + private String findUserByCredentials( + @NotNull WebAuthProviderDescriptor authProvider, + @NotNull Map authParameters, + boolean onlyActive // throws exception if user is inactive + ) throws DBException { + String userId = findUserByCredentials(authProvider, authParameters, onlyActive, false); + if (userId == null && authProvider.isCaseInsensitive()) { + // try to find user id with lower case is auth provider is case-insensitive + return findUserByCredentials(authProvider, authParameters, onlyActive, true); + } + return userId; } @Nullable - private String findUserByCredentials(WebAuthProviderDescriptor authProvider, Map authParameters) throws DBCException { - Map identCredentials = new LinkedHashMap<>(); + private String findUserByCredentials( + @NotNull WebAuthProviderDescriptor authProvider, + @NotNull Map authParameters, + boolean onlyActive, + boolean isCaseInsensitive + ) throws DBCException { + Map identCredentials = new LinkedHashMap<>(); String[] propNames = authParameters.keySet().toArray(new String[0]); for (AuthPropertyDescriptor prop : authProvider.getCredentialParameters(propNames)) { if (prop.isIdentifying()) { String propId = CommonUtils.toString(prop.getId()); - Object paramValue = authParameters.get(propId); - if (paramValue == null) { + if (authParameters.get(propId) == null) { throw new DBCException("Authentication parameter '" + prop.getId() + "' is missing"); } if (prop.getEncryption() == AuthPropertyEncryption.hash) { throw new DBCException("Hash encryption can't be used in identifying credentials"); } - identCredentials.put(propId, paramValue); + String paramValue = CommonUtils.toString(authParameters.get(propId)); + identCredentials.put( + propId, + isCaseInsensitive ? paramValue.toLowerCase() : paramValue + ); } } if (identCredentials.isEmpty()) { @@ -739,12 +1035,12 @@ private String findUserByCredentials(WebAuthProviderDescriptor authProvider, Map .append(joinAlias).append("CRED_VALUE=?"); } try (Connection dbCon = database.openConnection()) { - try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(sql.toString()))) { + try (PreparedStatement dbStat = dbCon.prepareStatement(sql.toString())) { dbStat.setString(1, authProvider.getId()); int param = 2; - for (Map.Entry credEntry : identCredentials.entrySet()) { + for (Map.Entry credEntry : identCredentials.entrySet()) { dbStat.setString(param++, credEntry.getKey()); - dbStat.setString(param++, CommonUtils.toString(credEntry.getValue())); + dbStat.setString(param++, credEntry.getValue()); } try (ResultSet dbResult = dbStat.executeQuery()) { @@ -760,7 +1056,7 @@ private String findUserByCredentials(WebAuthProviderDescriptor authProvider, Map } } - if (userId != null && !isActive) { + if (userId != null && onlyActive && !isActive) { throw new DBCException("User account is locked"); } @@ -775,17 +1071,25 @@ private String findUserByCredentials(WebAuthProviderDescriptor authProvider, Map @Override public Map getUserCredentials(String userId, String authProviderId) throws DBCException { WebAuthProviderDescriptor authProvider = getAuthProvider(authProviderId); + Map creds = getUserCredentials(authProvider, userId); + if (creds.isEmpty() && authProvider.isCaseInsensitive()) { + return getUserCredentials(authProvider, userId.toLowerCase()); + } + return creds; + } + + @NotNull + private Map getUserCredentials(WebAuthProviderDescriptor authProvider, String userId) throws DBCException { try (Connection dbCon = database.openConnection()) { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT CRED_ID,CRED_VALUE FROM {table_prefix}CB_USER_CREDENTIALS\n" + - "WHERE USER_ID=? AND PROVIDER_ID=?"))) { + "SELECT CRED_ID,CRED_VALUE FROM {table_prefix}CB_USER_CREDENTIALS\n" + + "WHERE USER_ID=? AND PROVIDER_ID=?")) { dbStat.setString(1, userId); dbStat.setString(2, authProvider.getId()); try (ResultSet dbResult = dbStat.executeQuery()) { Map credentials = new LinkedHashMap<>(); - while (dbResult.next()) { credentials.put(dbResult.getString(1), dbResult.getString(2)); } @@ -813,7 +1117,7 @@ public String[] getCurrentUserLinkedProviders() throws DBException { public String[] getUserLinkedProviders(@NotNull String userId) throws DBException { try (Connection dbCon = database.openConnection()) { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT DISTINCT PROVIDER_ID FROM {table_prefix}CB_USER_CREDENTIALS\n WHERE USER_ID=?"))) { + "SELECT DISTINCT PROVIDER_ID FROM {table_prefix}CB_USER_CREDENTIALS\n WHERE USER_ID=?")) { dbStat.setString(1, userId); try (ResultSet dbResult = dbStat.executeQuery()) { @@ -833,21 +1137,19 @@ public String[] getUserLinkedProviders(@NotNull String userId) throws DBExceptio @NotNull @Override - public SMPropertyDescriptor[] getMetaParametersBySubjectType(SMSubjectType subjectType) throws DBException { + public SMPropertyDescriptor[] getMetaParametersBySubjectType(SMSubjectType subjectType) { // First add global metas List props = new ArrayList<>( WebMetaParametersRegistry.getInstance().getMetaParameters(subjectType)); // Add metas from enabled auth providers - WebAppConfiguration appConfiguration = application.getAppConfiguration(); - if (appConfiguration instanceof WebAuthConfiguration) { - for (String apId : ((WebAuthConfiguration) appConfiguration).getEnabledAuthProviders()) { - WebAuthProviderDescriptor ap = WebAuthProviderRegistry.getInstance().getAuthProvider(apId); - if (ap != null) { - List metaProps = ap.getMetaParameters(SMSubjectType.team); - if (!CommonUtils.isEmpty(metaProps)) { - props.addAll(metaProps); - } + ServletAuthConfiguration authConfiguration = application.getAuthConfiguration(); + for (String apId : authConfiguration.getEnabledAuthProviders()) { + WebAuthProviderDescriptor ap = WebAuthProviderRegistry.getInstance().getAuthProvider(apId); + if (ap != null) { + List metaProps = ap.getMetaParameters(SMSubjectType.team); + if (!CommonUtils.isEmpty(metaProps)) { + props.addAll(metaProps); } } } @@ -864,19 +1166,30 @@ public SMPropertyDescriptor[] getMetaParametersBySubjectType(SMSubjectType subje @Override public SMTeam[] readAllTeams() throws DBCException { try (Connection dbCon = database.openConnection()) { + String defaultUserTeam = getDefaultUserTeam(); Map teams = new LinkedHashMap<>(); - try (Statement dbStat = dbCon.createStatement()) { - try (ResultSet dbResult = dbStat.executeQuery( - database.normalizeTableNames("SELECT * FROM {table_prefix}CB_TEAM ORDER BY TEAM_ID"))) { + String query = + """ + SELECT T.*, S.IS_SECRET_STORAGE FROM {table_prefix}CB_TEAM T, \ + {table_prefix}CB_AUTH_SUBJECT S \ + WHERE T.TEAM_ID IN (S.SUBJECT_ID, ?) ORDER BY TEAM_ID"""; + try (PreparedStatement dbPreparedStatement = dbCon.prepareStatement(query)) { + dbPreparedStatement.setString(1, defaultUserTeam); + try (ResultSet dbResult = dbPreparedStatement.executeQuery()) { while (dbResult.next()) { SMTeam team = fetchTeam(dbResult); teams.put(team.getTeamId(), team); } } - try (ResultSet dbResult = dbStat.executeQuery( - database.normalizeTableNames("SELECT SUBJECT_ID,PERMISSION_ID\n" + - "FROM {table_prefix}CB_AUTH_PERMISSIONS AP, {table_prefix}CB_TEAM R\n" + - "WHERE AP.SUBJECT_ID=R.TEAM_ID\n"))) { + } + query = """ + SELECT SUBJECT_ID,PERMISSION_ID + FROM {table_prefix}CB_AUTH_PERMISSIONS AP, {table_prefix}CB_TEAM R + WHERE AP.SUBJECT_ID IN (R.TEAM_ID,?) + """; + try (PreparedStatement dbPreparedStatement = dbCon.prepareStatement(query)) { + dbPreparedStatement.setString(1, defaultUserTeam); + try (ResultSet dbResult = dbPreparedStatement.executeQuery()) { while (dbResult.next()) { SMTeam team = teams.get(dbResult.getString(1)); if (team != null) { @@ -893,7 +1206,7 @@ public SMTeam[] readAllTeams() throws DBCException { } @Override - public SMTeam findTeam(String teamId) throws DBCException { + public SMTeam findTeam(@NotNull String teamId) throws DBCException { return Arrays.stream(readAllTeams()) .filter(r -> r.getTeamId().equals(teamId)) .findFirst().orElse(null); @@ -901,19 +1214,40 @@ public SMTeam findTeam(String teamId) throws DBCException { @NotNull @Override - public String[] getTeamMembers(String teamId) throws DBCException { + public String[] getTeamMembers(String teamId) throws DBException { + return getTeamMembersInfo(teamId).stream().map(SMTeamMemberInfo::userId).toArray(String[]::new); + } + + @NotNull + @Override + public List getTeamMembersInfo(@NotNull String teamId) throws DBException { try (Connection dbCon = database.openConnection()) { + Map usersRoles = new LinkedHashMap<>(); + if (getDefaultUserTeam().equals(teamId)) { + try (PreparedStatement dbStat = dbCon.prepareStatement("SELECT USER_ID FROM {table_prefix}CB_USER")) { + try (ResultSet dbResult = dbStat.executeQuery()) { + while (dbResult.next()) { + usersRoles.put(dbResult.getString(1), null); + } + } + } + } try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT USER_ID FROM {table_prefix}CB_USER_TEAM WHERE TEAM_ID=?"))) { + "SELECT USER_ID,TEAM_ROLE FROM {table_prefix}CB_USER_TEAM WHERE TEAM_ID=?") + ) { dbStat.setString(1, teamId); - List subjects = new ArrayList<>(); try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { - subjects.add(dbResult.getString(1)); + String userId = dbResult.getString(1); + String teamRole = dbResult.getString(2); + usersRoles.put(userId, teamRole); } } - return subjects.toArray(new String[0]); } + return usersRoles.entrySet() + .stream() + .map(entry -> new SMTeamMemberInfo(entry.getKey(), entry.getValue())) + .toList(); } catch (SQLException e) { throw new DBCException("Error while reading team members", e); } @@ -924,12 +1258,33 @@ private SMTeam fetchTeam(ResultSet dbResult) throws SQLException { return new SMTeam( dbResult.getString("TEAM_ID"), dbResult.getString("TEAM_NAME"), - dbResult.getString("TEAM_DESCRIPTION") + dbResult.getString("TEAM_DESCRIPTION"), + stringToBoolean(dbResult.getString("IS_SECRET_STORAGE")) + ); + } + + @NotNull + private SMUser fetchUser(ResultSet dbResult, boolean checkSecretStorage) throws SQLException { + Timestamp timestamp = dbResult.getTimestamp("CHANGE_DATE"); + Instant disableDate = timestamp != null ? timestamp.toInstant() : null; + return new SMUser( + dbResult.getString("USER_ID"), + stringToBoolean(dbResult.getString("IS_ACTIVE")), + dbResult.getString("DEFAULT_AUTH_ROLE"), + !checkSecretStorage || stringToBoolean(dbResult.getString("IS_SECRET_STORAGE")), + disableDate, + dbResult.getString("DISABLED_BY"), + dbResult.getString("DISABLE_REASON") ); } @Override - public void createTeam(String teamId, String name, String description, String grantor) throws DBCException { + public SMTeam createTeam( + @NotNull String teamId, + @Nullable String name, + @Nullable String description, + @NotNull String grantor + ) throws DBCException { if (CommonUtils.isEmpty(teamId)) { throw new DBCException("Empty team name is not allowed"); } @@ -938,10 +1293,10 @@ public void createTeam(String teamId, String name, String description, String gr } try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { - createAuthSubject(dbCon, teamId, SUBJECT_TEAM); + createAuthSubject(dbCon, teamId, SMSubjectType.team, true); try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_TEAM" + - "(TEAM_ID,TEAM_NAME,TEAM_DESCRIPTION,CREATE_TIME) VALUES(?,?,?,?)"))) { + "INSERT INTO {table_prefix}CB_TEAM" + + "(TEAM_ID,TEAM_NAME,TEAM_DESCRIPTION,CREATE_TIME) VALUES(?,?,?,?)")) { dbStat.setString(1, teamId); dbStat.setString(2, CommonUtils.notEmpty(name)); dbStat.setString(3, CommonUtils.notEmpty(description)); @@ -958,8 +1313,13 @@ public void createTeam(String teamId, String name, String description, String gr txn.commit(); } } catch (SQLException e) { - throw new DBCException("Error saving tem in database", e); + throw new DBCException("Error saving team in database", e); + } + SMTeam smTeam = new SMTeam(teamId, name, description, true); + for (String permission : getDefaultTeamPermissions()) { + smTeam.addPermission(permission); } + return smTeam; } protected String[] getDefaultTeamPermissions() { @@ -974,7 +1334,7 @@ public void updateTeam(String teamId, String name, String description) throws DB try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("UPDATE {table_prefix}CB_TEAM SET TEAM_NAME=?,TEAM_DESCRIPTION=? WHERE TEAM_ID=?"))) { + "UPDATE {table_prefix}CB_TEAM SET TEAM_NAME=?,TEAM_DESCRIPTION=? WHERE TEAM_ID=?")) { dbStat.setString(1, CommonUtils.notEmpty(name)); dbStat.setString(2, CommonUtils.notEmpty(description)); dbStat.setString(3, teamId); @@ -991,11 +1351,15 @@ public void updateTeam(String teamId, String name, String description) throws DB @Override public void deleteTeam(String teamId, boolean force) throws DBCException { + String defaultUsersTeam = getDefaultUserTeam(); + if (CommonUtils.isNotEmpty(defaultUsersTeam) && defaultUsersTeam.equals(teamId)) { + throw new DBCException("Default users team cannot be deleted"); + } try (Connection dbCon = database.openConnection()) { if (!force) { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT COUNT(*) FROM {table_prefix}CB_USER_TEAM WHERE TEAM_ID=?") - )) { + "SELECT COUNT(*) FROM {table_prefix}CB_USER_TEAM WHERE TEAM_ID=?") + ) { dbStat.setString(1, teamId); try (ResultSet dbResult = dbStat.executeQuery()) { if (dbResult.next()) { @@ -1012,13 +1376,13 @@ public void deleteTeam(String teamId, boolean force) throws DBCException { if (force) { JDBCUtils.executeStatement( dbCon, - database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER_TEAM WHERE TEAM_ID=?"), + "DELETE FROM {table_prefix}CB_USER_TEAM WHERE TEAM_ID=?", teamId ); } deleteAuthSubject(dbCon, teamId); try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("DELETE FROM {table_prefix}CB_TEAM WHERE TEAM_ID=?"))) { + "DELETE FROM {table_prefix}CB_TEAM WHERE TEAM_ID=?")) { dbStat.setString(1, teamId); dbStat.execute(); } @@ -1036,7 +1400,8 @@ public void deleteTeam(String teamId, boolean force) throws DBCException { // Subject functions @Override - public void setSubjectMetas(@NotNull String subjectId, @NotNull Map metaParameters) throws DBCException { + public void setSubjectMetas(@NotNull String subjectId, @NotNull Map metaParameters) throws DBException { + validateSubjectMetaValues(metaParameters); try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { cleanupSubjectMeta(dbCon, subjectId); @@ -1050,13 +1415,23 @@ public void setSubjectMetas(@NotNull String subjectId, @NotNull Map metaParameters) throws DBException { + Optional invalidValues = metaParameters.values() + .stream() + .filter(metaValue -> metaValue != null && metaValue.length() > 1024) // META_VALUE has max length of 1024 + .findAny(); + if (invalidValues.isPresent()) { + throw new DBException("One or more meta parameters contain invalid values. Please check the input and try again"); + } + } + @Override public void setSubjectPermissions(String subjectId, List permissionIds, String grantorId) throws DBException { // validatePermissions(SMConstants.SUBJECT_PERMISSION_SCOPE, permissionIds); try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { JDBCUtils.executeStatement(dbCon, - database.normalizeTableNames("DELETE FROM {table_prefix}CB_AUTH_PERMISSIONS WHERE SUBJECT_ID=?"), + "DELETE FROM {table_prefix}CB_AUTH_PERMISSIONS WHERE SUBJECT_ID=?", subjectId); insertPermissions(dbCon, subjectId, permissionIds.toArray(String[]::new), grantorId); txn.commit(); @@ -1067,11 +1442,16 @@ public void setSubjectPermissions(String subjectId, List permissionIds, addSubjectPermissionsUpdateEvent(subjectId, null); } + + public void initialize() throws DBException { + } + private void insertPermissions(Connection dbCon, String subjectId, String[] permissionIds, String grantorId) throws SQLException { if (!ArrayUtils.isEmpty(permissionIds)) { - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_AUTH_PERMISSIONS" + - "(SUBJECT_ID,PERMISSION_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)")) + try ( + PreparedStatement dbStat = dbCon.prepareStatement( + "INSERT INTO {table_prefix}CB_AUTH_PERMISSIONS" + + "(SUBJECT_ID,PERMISSION_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)") ) { for (String permission : permissionIds) { dbStat.setString(1, subjectId); @@ -1090,7 +1470,7 @@ public Set getSubjectPermissions(String subjectId) throws DBException { try (Connection dbCon = database.openConnection()) { Set permissions = new HashSet<>(); try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT PERMISSION_ID FROM {table_prefix}CB_AUTH_PERMISSIONS WHERE SUBJECT_ID=?"))) { + "SELECT PERMISSION_ID FROM {table_prefix}CB_AUTH_PERMISSIONS WHERE SUBJECT_ID=?")) { dbStat.setString(1, subjectId); try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { @@ -1110,10 +1490,8 @@ public Set getUserPermissions(String userId) throws DBException { try (Connection dbCon = database.openConnection()) { Set permissions = new HashSet<>(); try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames( - "SELECT DISTINCT AP.PERMISSION_ID FROM {table_prefix}CB_AUTH_PERMISSIONS AP, {table_prefix}CB_USER_TEAM UR\n" + - "WHERE UR.TEAM_ID=AP.SUBJECT_ID AND UR.USER_ID=?" - ) + "SELECT DISTINCT AP.PERMISSION_ID FROM {table_prefix}CB_AUTH_PERMISSIONS AP, {table_prefix}CB_USER_TEAM UR\n" + + "WHERE UR.TEAM_ID = AP.SUBJECT_ID AND UR.USER_ID=?" )) { dbStat.setString(1, userId); try (ResultSet dbResult = dbStat.executeQuery()) { @@ -1123,7 +1501,7 @@ public Set getUserPermissions(String userId) throws DBException { } } try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT PERMISSION_ID FROM {table_prefix}CB_AUTH_PERMISSIONS WHERE SUBJECT_ID=?")) + "SELECT PERMISSION_ID FROM {table_prefix}CB_AUTH_PERMISSIONS WHERE SUBJECT_ID=?") ) { dbStat.setString(1, userId); try (ResultSet dbResult = dbStat.executeQuery()) { @@ -1139,6 +1517,7 @@ public Set getUserPermissions(String userId) throws DBException { } } + protected Set getUserPermissions(String userId, String authRole) throws DBException { return getUserPermissions(userId); } @@ -1149,9 +1528,7 @@ protected Set getUserPermissions(String userId, String authRole) throws @Override public boolean isSessionPersisted(String id) throws DBException { try (Connection dbCon = database.openConnection()) { - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT 1 FROM {table_prefix}CB_SESSION WHERE SESSION_ID=?")) - ) { + try (PreparedStatement dbStat = dbCon.prepareStatement("SELECT 1 FROM {table_prefix}CB_SESSION WHERE SESSION_ID=?")) { dbStat.setString(1, id); try (ResultSet dbResult = dbStat.executeQuery()) { if (dbResult.next()) { @@ -1171,14 +1548,12 @@ private String createSmSession( @NotNull Map parameters, @NotNull SMSessionType sessionType, Connection dbCon - ) throws SQLException, DBException { + ) throws SQLException { var sessionId = UUID.randomUUID().toString(); try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames( - "INSERT INTO {table_prefix}CB_SESSION(SESSION_ID, APP_SESSION_ID, USER_ID,CREATE_TIME,LAST_ACCESS_TIME," + - "LAST_ACCESS_REMOTE_ADDRESS,LAST_ACCESS_USER_AGENT,LAST_ACCESS_INSTANCE_ID, SESSION_TYPE) " + - "VALUES(?,?,?,?,?,?,?,?,?)" - ) + "INSERT INTO {table_prefix}CB_SESSION(SESSION_ID, APP_SESSION_ID, USER_ID,CREATE_TIME,LAST_ACCESS_TIME," + + "LAST_ACCESS_REMOTE_ADDRESS,LAST_ACCESS_USER_AGENT,LAST_ACCESS_INSTANCE_ID, SESSION_TYPE) " + + "VALUES(?,?,?,?,?,?,?,?,?)" )) { dbStat.setString(1, sessionId); dbStat.setString(2, appSessionId); @@ -1200,10 +1575,13 @@ private String createSmSession( @Override public SMAuthInfo authenticateAnonymousUser(@NotNull String appSessionId, @NotNull Map sessionParameters, @NotNull SMSessionType sessionType) throws DBException { + if (!application.getAppConfiguration().isAnonymousAccessEnabled()) { + throw new SMException("Anonymous access restricted"); + } try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { var smSessionId = createSmSession(appSessionId, null, sessionParameters, sessionType, dbCon); - var smTokens = generateNewSessionToken(smSessionId, null, null, dbCon); + var smTokens = generateNewSessionToken(smSessionId, null, null, dbCon, false); var permissions = getAnonymousUserPermissions(); txn.commit(); return SMAuthInfo.successMainSession( @@ -1212,7 +1590,8 @@ public SMAuthInfo authenticateAnonymousUser(@NotNull String appSessionId, @NotNu smTokens.getSmRefreshToken(), new SMAuthPermissions(null, smSessionId, permissions), Map.of(), - null + null, + appSessionId ); } } catch (SQLException e) { @@ -1220,7 +1599,7 @@ public SMAuthInfo authenticateAnonymousUser(@NotNull String appSessionId, @NotNu } } - private Set getAnonymousUserPermissions() throws DBException { + protected Set getAnonymousUserPermissions() throws DBException { var anonymousUserTeam = application.getAppConfiguration().getAnonymousUserTeam(); return getSubjectPermissions(anonymousUserTeam); } @@ -1233,12 +1612,16 @@ public SMAuthInfo authenticate( @NotNull SMSessionType sessionType, @NotNull String authProviderId, @Nullable String authProviderConfigurationId, - @NotNull Map userCredentials + @NotNull Map userCredentials, + boolean forceSessionsLogout ) throws DBException { + if (isProviderDisabled(authProviderId, authProviderConfigurationId)) { + throw new SMException("Unsupported authentication provider: " + authProviderId); + } var authProgressMonitor = new LoggingProgressMonitor(log); + boolean isMainSession = previousSmSessionId == null; try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { - boolean isMainSession = previousSmSessionId == null; Map securedUserIdentifyingCredentials = userCredentials; WebAuthProviderDescriptor authProviderDescriptor = getAuthProvider(authProviderId); var authProviderInstance = authProviderDescriptor.getInstance(); @@ -1247,21 +1630,44 @@ public SMAuthInfo authenticate( ? null : application.getAuthConfiguration().getAuthProviderConfiguration(authProviderConfigurationId); - if (SMAuthProviderExternal.class.isAssignableFrom(authProviderInstance.getClass())) { - var authProviderExternal = (SMAuthProviderExternal) authProviderInstance; - securedUserIdentifyingCredentials = authProviderExternal.authExternalUser( - authProgressMonitor, - providerConfig, - userCredentials - ); - } - var filteredUserCreds = filterSecuredUserData( securedUserIdentifyingCredentials, authProviderDescriptor ); - - var authAttemptId = createNewAuthAttempt( + String authAttemptId; + if (SMAuthProviderExternal.class.isAssignableFrom(authProviderInstance.getClass())) { + var authProviderExternal = (SMAuthProviderExternal) authProviderInstance; + try { + securedUserIdentifyingCredentials = authProviderExternal.authExternalUser( + authProgressMonitor, + providerConfig, + userCredentials + ); + } catch (DBException e) { + createNewAuthAttempt( + SMAuthStatus.ERROR, + authProviderId, + authProviderConfigurationId, + filteredUserCreds, + appSessionId, + previousSmSessionId, + sessionType, + sessionParameters, + isMainSession, + forceSessionsLogout + ); + throw e; + } + } + boolean isFederatedAuth = SMAuthProviderFederated.class.isAssignableFrom(authProviderInstance.getClass()); + if (isFederatedAuth) { + String userOrigin = JSONUtils.getString(userCredentials, SMConstants.USER_ORIGIN); + if (CommonUtils.isEmpty(userOrigin)) { + throw new SMException("User origin not found in authentication data"); + } + filteredUserCreds.put(SMConstants.USER_ORIGIN, modifyOrigin(userOrigin)); + } + authAttemptId = createNewAuthAttempt( SMAuthStatus.IN_PROGRESS, authProviderId, authProviderConfigurationId, @@ -1270,27 +1676,51 @@ public SMAuthInfo authenticate( previousSmSessionId, sessionType, sessionParameters, - isMainSession + isMainSession, + forceSessionsLogout ); - if (SMAuthProviderFederated.class.isAssignableFrom(authProviderInstance.getClass())) { + if (isFederatedAuth) { + String userOrigin = JSONUtils.getString(filteredUserCreds, SMConstants.USER_ORIGIN); //async auth var authProviderFederated = (SMAuthProviderFederated) authProviderInstance; - var redirectUrl = buildRedirectLink(authProviderFederated.getSignInLink(authProviderConfigurationId, Map.of()), - authAttemptId); - Map authData = Map.of(new SMAuthConfigurationReference(authProviderId, - authProviderConfigurationId), filteredUserCreds); - return SMAuthInfo.inProgress(authAttemptId, redirectUrl, authData); + String signInLink = buildRedirectLink( + authProviderFederated.getSignInLink(authProviderConfigurationId, userOrigin), + authAttemptId + ); + String signOutLink = authProviderFederated.getCommonSignOutLink(authProviderConfigurationId, + providerConfig.getParameters(), userOrigin + ); + Map authData = Map.of( + new SMAuthConfigurationReference(authProviderId, authProviderConfigurationId), + filteredUserCreds + ); + return SMAuthInfo.inProgress( + authAttemptId, + signInLink, + signOutLink, + authData, + isMainSession, + forceSessionsLogout, + appSessionId + ); } txn.commit(); return finishAuthentication( SMAuthInfo.inProgress( authAttemptId, null, - Map.of(new SMAuthConfigurationReference(authProviderId, authProviderConfigurationId), securedUserIdentifyingCredentials) + null, + Map.of( + new SMAuthConfigurationReference(authProviderId, authProviderConfigurationId), + securedUserIdentifyingCredentials + ), + isMainSession, + forceSessionsLogout, + appSessionId ), true, - false + forceSessionsLogout ); } } catch (SQLException e) { @@ -1298,6 +1728,23 @@ public SMAuthInfo authenticate( } } + @NotNull + protected String modifyOrigin(@NotNull String origin) { + StringBuilder finalOrigin = new StringBuilder(); + URI uri = URI.create(origin); + finalOrigin.append(uri.getScheme()) + .append("://") + .append(uri.getHost()); + if (uri.getPort() > 0 && application.getServerPort() != uri.getPort()) { + finalOrigin.append(":").append(application.getServerPort()); + } else { + return origin; + } + finalOrigin.append(uri.getPath()); + + return finalOrigin.toString(); + } + private Map filterSecuredUserData( Map userIdentifyingCredentials, WebAuthProviderDescriptor authProviderDescriptor @@ -1322,17 +1769,27 @@ private String createNewAuthAttempt( String prevSessionId, SMSessionType sessionType, Map sessionParameters, - boolean isMainSession + boolean isMainSession, + boolean forceSessionsLogout ) throws DBException { String authAttemptId = UUID.randomUUID().toString(); try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { + WebAuthProviderDescriptor authProviderDescriptor = getAuthProvider(authProviderId); + if (smConfig.isCheckBruteforce() + && authProviderDescriptor.getInstance() instanceof SMBruteForceProtected bruteforceProtected) { + Object inputUsername = bruteforceProtected.getInputUsername(authData); + if (inputUsername != null) { + BruteForceUtils.checkBruteforce(smConfig, + getLatestUserLogins(dbCon, authProviderId, inputUsername.toString())); + } + } try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames( - "INSERT INTO {table_prefix}CB_AUTH_ATTEMPT" + - "(AUTH_ID,AUTH_STATUS,APP_SESSION_ID,SESSION_TYPE,APP_SESSION_STATE,SESSION_ID) " + - "VALUES(?,?,?,?,?,?)" - ) + "INSERT INTO {table_prefix}CB_AUTH_ATTEMPT" + + "(AUTH_ID,AUTH_STATUS,APP_SESSION_ID,SESSION_TYPE,APP_SESSION_STATE," + + "SESSION_ID,IS_MAIN_AUTH,AUTH_USERNAME,FORCE_SESSION_LOGOUT,IS_SERVICE_AUTH) " + + + "VALUES(?,?,?,?,?,?,?,?,?,?)" )) { dbStat.setString(1, authAttemptId); dbStat.setString(2, status.toString()); @@ -1344,15 +1801,27 @@ private String createNewAuthAttempt( } else { dbStat.setNull(6, Types.VARCHAR); } + dbStat.setString(7, isMainSession ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); + if (this.getAuthProvider(authProviderId).getInstance() instanceof SMBruteForceProtected bruteforceProtected) { + Object inputUsername = bruteforceProtected.getInputUsername(authData); + if (inputUsername != null) { + dbStat.setString(8, inputUsername.toString()); + } else { + dbStat.setString(8, null); + } + } else { + dbStat.setString(8, null); + } + dbStat.setString(9, booleanToString(forceSessionsLogout)); + boolean isServiceAuth = isMainSession && authProviderDescriptor.isServiceProvider(); + dbStat.setString(10, booleanToString(isServiceAuth)); dbStat.execute(); } try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames( - "INSERT INTO {table_prefix}CB_AUTH_ATTEMPT_INFO" + - "(AUTH_ID,AUTH_PROVIDER_ID,AUTH_PROVIDER_CONFIGURATION_ID,AUTH_STATE) " + - "VALUES(?,?,?,?)" - ) + "INSERT INTO {table_prefix}CB_AUTH_ATTEMPT_INFO" + + "(AUTH_ID,AUTH_PROVIDER_ID,AUTH_PROVIDER_CONFIGURATION_ID,AUTH_STATE) " + + "VALUES(?,?,?,?)" )) { dbStat.setString(1, authAttemptId); dbStat.setString(2, authProviderId); @@ -1368,6 +1837,37 @@ private String createNewAuthAttempt( } } + private List getLatestUserLogins(Connection dbCon, String authProviderId, String inputLogin) throws SQLException { + List userLoginRecords = new ArrayList<>(); + try (PreparedStatement dbStat = dbCon.prepareStatement( + "SELECT" + + " attempt.AUTH_STATUS," + + " attempt.CREATE_TIME" + + " FROM" + + " {table_prefix}CB_AUTH_ATTEMPT attempt" + + " JOIN" + + " {table_prefix}CB_AUTH_ATTEMPT_INFO info ON attempt.AUTH_ID = info.AUTH_ID" + + " WHERE AUTH_PROVIDER_ID = ? AND AUTH_USERNAME = ? AND attempt.CREATE_TIME > ?" + + " ORDER BY attempt.CREATE_TIME DESC " + + database.getDialect().getOffsetLimitQueryPart(0, smConfig.getMaxFailedLogin()) + )) { + dbStat.setString(1, authProviderId); + dbStat.setString(2, inputLogin); + dbStat.setTimestamp(3, + Timestamp.valueOf(LocalDateTime.now().minusSeconds(smConfig.getBlockLoginPeriod()))); + try (ResultSet dbResult = dbStat.executeQuery()) { + while (dbResult.next()) { + UserLoginRecord loginDto = new UserLoginRecord( + SMAuthStatus.valueOf(dbResult.getString(1)), + dbResult.getTimestamp(2).toLocalDateTime() + ); + userLoginRecords.add(loginDto); + } + } + } + return userLoginRecords; + } + private boolean isSmSessionNotExpired(String prevSessionId) { //TODO: implement after we start tracking user logout return true; @@ -1378,14 +1878,21 @@ public void updateAuthStatus( @NotNull String authId, @NotNull SMAuthStatus authStatus, @NotNull Map authInfo, - @Nullable String error + @Nullable String error, + @Nullable String errorCode ) throws DBException { var existAuthInfo = getAuthStatus(authId); if (existAuthInfo.getAuthStatus() != SMAuthStatus.IN_PROGRESS) { throw new SMException("Authorization already finished and cannot be updated"); } var authSessionInfo = readAuthAttemptSessionInfo(authId); - updateAuthStatus(authId, authStatus, authInfo, error, authSessionInfo.getSmSessionId()); + updateAuthStatus(authId, authStatus, authInfo, error, authSessionInfo.getSmSessionId(), errorCode); + if (authStatus == SMAuthStatus.ERROR) { + SMAuthInfo errorInfo = getAuthStatus(authId, false); + application.getEventController().addEvent( + new WSAuthEvent(errorInfo) + ); + } } private void updateAuthStatus( @@ -1393,16 +1900,18 @@ private void updateAuthStatus( @NotNull SMAuthStatus authStatus, @NotNull Map authInfo, @Nullable String error, - @Nullable String smSessionId + @Nullable String smSessionId, + @Nullable String errorCode ) throws DBException { try (Connection dbCon = database.openConnection(); JDBCTransaction txn = new JDBCTransaction(dbCon)) { - try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames( - "UPDATE {table_prefix}CB_AUTH_ATTEMPT SET AUTH_STATUS=?,AUTH_ERROR=?,SESSION_ID=? WHERE AUTH_ID=?" - ))) { + try (PreparedStatement dbStat = dbCon.prepareStatement( + "UPDATE {table_prefix}CB_AUTH_ATTEMPT SET AUTH_STATUS=?,AUTH_ERROR=?,SESSION_ID=?,ERROR_CODE=? WHERE AUTH_ID=?" + )) { dbStat.setString(1, authStatus.toString()); JDBCUtils.setStringOrNull(dbStat, 2, error); JDBCUtils.setStringOrNull(dbStat, 3, smSessionId); - dbStat.setString(4, authId); + JDBCUtils.setStringOrNull(dbStat, 4, errorCode); + dbStat.setString(5, authId); if (dbStat.executeUpdate() <= 0) { throw new DBCException("Auth attempt '" + authId + "' doesn't exist"); } @@ -1412,11 +1921,10 @@ private void updateAuthStatus( SMAuthConfigurationReference providerId = entry.getKey(); String authJson = gson.toJson(entry.getValue()); boolean configIdExist = providerId.getAuthProviderConfigurationId() != null; - var sqlBuilder = new StringBuilder(); - sqlBuilder.append("UPDATE {table_prefix}CB_AUTH_ATTEMPT_INFO SET AUTH_STATE=? ") - .append("WHERE AUTH_ID=? AND AUTH_PROVIDER_ID=? AND ") - .append(configIdExist ? "AUTH_PROVIDER_CONFIGURATION_ID=?" : "AUTH_PROVIDER_CONFIGURATION_ID IS NULL"); - try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(sqlBuilder.toString()))) { + String sqlBuilder = "UPDATE {table_prefix}CB_AUTH_ATTEMPT_INFO SET AUTH_STATE=? " + + "WHERE AUTH_ID=? AND AUTH_PROVIDER_ID=? AND " + + (configIdExist ? "AUTH_PROVIDER_CONFIGURATION_ID=?" : "AUTH_PROVIDER_CONFIGURATION_ID IS NULL"); + try (PreparedStatement dbStat = dbCon.prepareStatement(sqlBuilder)) { dbStat.setString(1, authJson); dbStat.setString(2, authId); dbStat.setString(3, providerId.getAuthProviderId()); @@ -1425,10 +1933,10 @@ private void updateAuthStatus( } if (dbStat.executeUpdate() <= 0) { try (PreparedStatement dbStatIns = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_AUTH_ATTEMPT_INFO " + - "(AUTH_ID,AUTH_PROVIDER_ID,AUTH_PROVIDER_CONFIGURATION_ID,AUTH_STATE) " - + "VALUES(?,?,?,?)") - )) { + "INSERT INTO {table_prefix}CB_AUTH_ATTEMPT_INFO " + + "(AUTH_ID,AUTH_PROVIDER_ID,AUTH_PROVIDER_CONFIGURATION_ID,AUTH_STATE) " + + "VALUES(?,?,?,?)") + ) { dbStatIns.setString(1, authId); dbStatIns.setString(2, providerId.getAuthProviderId()); dbStatIns.setString(3, providerId.getAuthProviderConfigurationId()); @@ -1452,7 +1960,8 @@ public SMAuthInfo getAuthStatus(@NotNull String authId) throws DBException { SMAuthStatus.EXPIRED, smAuthInfo.getAuthData(), null, - smAuthInfo.getAuthPermissions().getSessionId() + smAuthInfo.getAuthPermissions().getSessionId(), + smAuthInfo.getErrorCode() ); } return smAuthInfo; @@ -1463,10 +1972,13 @@ private SMAuthInfo getAuthStatus(@NotNull String authId, boolean readExpiredData SMAuthStatus smAuthStatus; String authError; String smSessionId; + String errorCode; + boolean forceSessionsLogout; + boolean isMainAuth; + String appSessionId; try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames( - "SELECT AUTH_STATUS,AUTH_ERROR,SESSION_ID FROM {table_prefix}CB_AUTH_ATTEMPT WHERE AUTH_ID=?" - ) + "SELECT AUTH_STATUS,AUTH_ERROR,SESSION_ID,IS_MAIN_AUTH,ERROR_CODE,FORCE_SESSION_LOGOUT,APP_SESSION_ID" + + " FROM {table_prefix}CB_AUTH_ATTEMPT WHERE AUTH_ID=?" )) { dbStat.setString(1, authId); try (ResultSet dbResult = dbStat.executeQuery()) { @@ -1476,17 +1988,21 @@ private SMAuthInfo getAuthStatus(@NotNull String authId, boolean readExpiredData smAuthStatus = SMAuthStatus.valueOf(dbResult.getString(1)); authError = dbResult.getString(2); smSessionId = dbResult.getString(3); + isMainAuth = CHAR_BOOL_TRUE.equals(dbResult.getString(4)); + errorCode = dbResult.getString(5); + forceSessionsLogout = CHAR_BOOL_TRUE.equals(dbResult.getString(6)); + appSessionId = dbResult.getString(7); } } Map authData = new LinkedHashMap<>(); - String redirectUrl = null; + String signInLink = null; + String signOutLink = null; try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames( - "SELECT AUTH_PROVIDER_ID,AUTH_PROVIDER_CONFIGURATION_ID,AUTH_STATE " + - "FROM {table_prefix}CB_AUTH_ATTEMPT_INFO " - + "WHERE AUTH_ID=? ORDER BY CREATE_TIME" - ) + """ + SELECT AUTH_PROVIDER_ID,AUTH_PROVIDER_CONFIGURATION_ID,AUTH_STATE \ + FROM {table_prefix}CB_AUTH_ATTEMPT_INFO \ + WHERE AUTH_ID=? ORDER BY CREATE_TIME""" )) { dbStat.setString(1, authId); try (ResultSet dbResult = dbStat.executeQuery()) { @@ -1497,10 +2013,19 @@ private SMAuthInfo getAuthStatus(@NotNull String authId, boolean readExpiredData if (authProviderConfiguration != null) { WebAuthProviderDescriptor authProviderDescriptor = getAuthProvider(authProviderId); var authProviderInstance = authProviderDescriptor.getInstance(); - if (SMAuthProviderFederated.class.isAssignableFrom(authProviderInstance.getClass())) { - redirectUrl = buildRedirectLink(((SMAuthProviderFederated) authProviderInstance).getRedirectLink( - authProviderConfiguration, - Map.of()), authId); + if (authProviderInstance instanceof SMAuthProviderFederated providerFederated) { + String userOrigin = JSONUtils.getString(authProviderData, SMConstants.USER_ORIGIN); + if(CommonUtils.isNotEmpty(userOrigin)){ + signInLink = buildRedirectLink( + providerFederated.getRedirectLink(authProviderConfiguration, Map.of(), userOrigin), + authId + ); + signOutLink = providerFederated.getUserSignOutLink( + application.getAuthConfiguration() + .getAuthProviderConfiguration(authProviderConfiguration), + authProviderData, userOrigin + ); + } } } @@ -1510,30 +2035,33 @@ private SMAuthInfo getAuthStatus(@NotNull String authId, boolean readExpiredData } if (smAuthStatus != SMAuthStatus.SUCCESS) { - switch (smAuthStatus) { - case IN_PROGRESS: - return SMAuthInfo.inProgress(authId, redirectUrl, authData); - case ERROR: - return SMAuthInfo.error(authId, authError); - case EXPIRED: - return SMAuthInfo.expired(authId, readExpiredData ? authData : Map.of()); - default: - throw new SMException("Unknown auth status:" + smAuthStatus); - } + return switch (smAuthStatus) { + case IN_PROGRESS -> + SMAuthInfo.inProgress(authId, signInLink, signOutLink, authData, isMainAuth, forceSessionsLogout, appSessionId); + case ERROR -> SMAuthInfo.error(authId, authError, isMainAuth, errorCode, appSessionId); + case EXPIRED -> SMAuthInfo.expired(authId, readExpiredData ? authData : Map.of(), isMainAuth, appSessionId); + default -> throw new SMException("Unknown auth status:" + smAuthStatus); + }; } SMTokens smTokens = findTokenBySmSession(smSessionId); SMAuthPermissions authPermissions = getTokenPermissions(smTokens.getSmAccessToken()); - String authRole = readTokenAuthRole(smTokens.getSmAccessToken()); - var successAuthStatus = SMAuthInfo.successMainSession( - authId, - smTokens.getSmAccessToken(), - smTokens.getSmRefreshToken(), - authPermissions, - authData, - authRole - ); - return successAuthStatus; + + if (isMainAuth) { + String authRole = readTokenAuthRole(smTokens.getSmAccessToken()); + return SMAuthInfo.successMainSession( + authId, + smTokens.getSmAccessToken(), + smTokens.getSmRefreshToken(), + authPermissions, + authData, + authRole, + appSessionId + ); + } else { + //TODO remove permissions from child session + return SMAuthInfo.successChildSession(authId, authPermissions, authData, appSessionId); + } } catch (SQLException e) { throw new DBException("Error while read auth info", e); } @@ -1559,7 +2087,8 @@ public SMAuthInfo restoreUserSession(@NotNull String appSessionId) throws DBExce latestActiveSmTokens.getRefreshToken(), getTokenPermissions(latestActiveSmTokens.getAccessToken()), mergedData, - readTokenAuthRole(latestActiveSmTokens.getAccessToken()) + readTokenAuthRole(latestActiveSmTokens.getAccessToken()), + appSessionId ); } @@ -1567,8 +2096,8 @@ private List findAllSmSessionAuthData(String smSessionId) throws DBE try (var dbCon = database.openConnection()) { List authAttemptIds = JDBCUtils.queryStrings(dbCon, - database.normalizeTableNames("SELECT AUTH_ID FROM {table_prefix}CB_AUTH_ATTEMPT " + - "WHERE SESSION_ID=? AND AUTH_STATUS IN (?,?) ORDER BY CREATE_TIME"), + "SELECT AUTH_ID FROM {table_prefix}CB_AUTH_ATTEMPT " + + "WHERE SESSION_ID=? AND AUTH_STATUS IN (?,?) ORDER BY CREATE_TIME", smSessionId, SMAuthStatus.SUCCESS.name(), SMAuthStatus.EXPIRED.name() ); List result = new ArrayList<>(); @@ -1592,7 +2121,7 @@ public SMTokens refreshSession(@NotNull String refreshToken) throws DBException var currentUserCreds = getCurrentUserCreds(); var currentUserAccessToken = currentUserCreds.getSmAccessToken(); - var smTokenInfo = readAccessTokenInfo(currentUserAccessToken); + SMTokenInfo smTokenInfo = readAccessTokenInfo(currentUserAccessToken); if (!smTokenInfo.getRefreshToken().equals(refreshToken)) { throw new SMException("Invalid refresh token"); @@ -1603,8 +2132,8 @@ public SMTokens refreshSession(@NotNull String refreshToken) throws DBException return generateNewSessionToken( smTokenInfo.getSessionId(), smTokenInfo.getUserId(), - updateUserAuthRoleIfNeeded(smTokenInfo.getUserId(), null), - dbCon); + updateUserAuthRoleIfNeeded(smTokenInfo.getUserId(), null), dbCon, smTokenInfo.isServiceToken() + ); } catch (SQLException e) { throw new DBException("Error refreshing sm session", e); } @@ -1613,16 +2142,27 @@ public SMTokens refreshSession(@NotNull String refreshToken) throws DBException private void invalidateUserTokens(String smToken) throws DBCException { try (Connection dbCon = database.openConnection()) { JDBCUtils.executeStatement( - dbCon, database.normalizeTableNames("DELETE FROM {table_prefix}CB_AUTH_TOKEN WHERE TOKEN_ID=?"), smToken); + dbCon, "DELETE FROM {table_prefix}CB_AUTH_TOKEN WHERE TOKEN_ID=?", smToken); + } catch (SQLException e) { + throw new DBCException("Session invalidation failed", e); + } + } + + @Override + public void invalidateAllTokens() throws DBCException { + try (Connection dbCon = database.openConnection()) { + JDBCUtils.executeStatement( + dbCon, "DELETE FROM {table_prefix}CB_AUTH_TOKEN"); } catch (SQLException e) { throw new DBCException("Session invalidation failed", e); } + application.getEventController().addEvent(new WSUserCloseSessionsEvent(List.of(), getSmSessionId(), getUserId())); } - private void invalidateAllUserTokens(String userId) throws DBCException { + private void invalidateAllUserTokens(@NotNull String userId) throws DBCException { try (Connection dbCon = database.openConnection()) { JDBCUtils.executeStatement( - dbCon, database.normalizeTableNames("DELETE FROM {table_prefix}CB_AUTH_TOKEN WHERE USER_ID=?"), userId); + dbCon, "DELETE FROM {table_prefix}CB_AUTH_TOKEN WHERE USER_ID=?", userId); } catch (SQLException e) { throw new DBCException("Session invalidation failed", e); } @@ -1640,7 +2180,7 @@ private SMCredentials getCurrentUserCreds() throws SMException { private SMTokens findTokenBySmSession(String smSessionId) throws DBException { try (Connection dbCon = database.openConnection(); PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT TOKEN_ID, REFRESH_TOKEN_ID FROM {table_prefix}CB_AUTH_TOKEN WHERE SESSION_ID=?")) + "SELECT TOKEN_ID, REFRESH_TOKEN_ID FROM {table_prefix}CB_AUTH_TOKEN WHERE SESSION_ID=?") ) { dbStat.setString(1, smSessionId); try (var dbResult = dbStat.executeQuery()) { @@ -1658,13 +2198,12 @@ private SMTokens findTokenBySmSession(String smSessionId) throws DBException { private SMTokenInfo findTokenByAppSession(@NotNull String appSessionId) throws DBException { try (var dbCon = database.openConnection(); var dbStat = dbCon.prepareStatement( - database.normalizeTableNames( - "SELECT CAT.TOKEN_ID FROM {table_prefix}CB_AUTH_TOKEN CAT " + - " JOIN {table_prefix}CB_SESSION CS ON CAT.SESSION_ID = CS.SESSION_ID " + - " WHERE CS.APP_SESSION_ID = ? AND CAT.USER_ID IS NOT NULL " + - " AND CAT.EXPIRATION_TIME > CURRENT_TIMESTAMP" + - " ORDER BY CAT.EXPIRATION_TIME DESC" - ) + """ + SELECT CAT.TOKEN_ID FROM {table_prefix}CB_AUTH_TOKEN CAT \ + JOIN {table_prefix}CB_SESSION CS ON CAT.SESSION_ID = CS.SESSION_ID \ + WHERE CS.APP_SESSION_ID = ? AND CAT.USER_ID IS NOT NULL \ + AND CAT.EXPIRATION_TIME > CURRENT_TIMESTAMP\ + ORDER BY CAT.EXPIRATION_TIME DESC""" ) ) { dbStat.setString(1, appSessionId); @@ -1682,10 +2221,11 @@ private SMTokenInfo findTokenByAppSession(@NotNull String appSessionId) throws D private SMTokenInfo readAccessTokenInfo(String smAccessToken) throws DBException { try (Connection dbCon = database.openConnection(); - PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT REFRESH_TOKEN_ID,SESSION_ID,USER_ID,REFRESH_TOKEN_EXPIRATION_TIME,AUTH_ROLE FROM " + - "{table_prefix}CB_AUTH_TOKEN WHERE TOKEN_ID=?") - ) + PreparedStatement dbStat = dbCon.prepareStatement( + """ + SELECT REFRESH_TOKEN_ID,SESSION_ID,USER_ID,REFRESH_TOKEN_EXPIRATION_TIME,AUTH_ROLE,IS_SERVICE\ + FROM {table_prefix}CB_AUTH_TOKEN WHERE TOKEN_ID=?""" + ) ) { dbStat.setString(1, smAccessToken); try (var dbResult = dbStat.executeQuery()) { @@ -1700,7 +2240,8 @@ private SMTokenInfo readAccessTokenInfo(String smAccessToken) throws DBException if (isTokenExpired(expiredDate)) { throw new SMRefreshTokenExpiredException("Refresh token expired"); } - return new SMTokenInfo(smAccessToken, refreshToken, sessionId, userId, authRole); + boolean isService = stringToBoolean(dbResult.getString(6)); + return new SMTokenInfo(smAccessToken, refreshToken, sessionId, userId, authRole, isService); } } catch (SQLException e) { throw new DBCException("Error reading token info in database", e); @@ -1714,14 +2255,17 @@ private boolean isTokenExpired(Timestamp tokenExpiredDate) { @Override public SMAuthInfo finishAuthentication(@NotNull String authId) throws DBException { SMAuthInfo authInfo = getAuthStatus(authId); - return finishAuthentication(authInfo, false, true); + SMAuthInfo finalAuthInfo = finishAuthentication(authInfo, false, authInfo.isForceSessionsLogout()); + application.getEventController().addEvent(new WSAuthEvent(finalAuthInfo)); + return finalAuthInfo; } - private SMAuthInfo finishAuthentication( + protected SMAuthInfo finishAuthentication( @NotNull SMAuthInfo authInfo, - boolean forceExpireAuthAfterSuccess, - boolean saveSecuredCreds + boolean isSyncAuth, + boolean forceSessionsLogout ) throws DBException { + boolean isAsyncAuth = !isSyncAuth; String authId = authInfo.getAuthAttemptId(); if (authInfo.getAuthStatus() != SMAuthStatus.IN_PROGRESS) { throw new SMException("Authorization has already been completed with status: " + authInfo.getAuthStatus()); @@ -1733,23 +2277,26 @@ private SMAuthInfo finishAuthentication( DBRProgressMonitor finishAuthMonitor = new LoggingProgressMonitor(log); AuthAttemptSessionInfo authAttemptSessionInfo = readAuthAttemptSessionInfo(authId); - boolean isMainAuthSession = authAttemptSessionInfo.getSmSessionId() == null; + boolean isMainAuthSession = authAttemptSessionInfo.isMainAuth(); SMTokens smTokens = null; SMAuthPermissions permissions = null; String activeUserId = null; if (!isMainAuthSession) { var accessToken = findTokenBySmSession(authAttemptSessionInfo.getSmSessionId()).getSmAccessToken(); - //this is an additional authorization and we should to return the original permissions and userId + //this is an additional authorization, and we should to return the original permissions and userId permissions = getTokenPermissions(accessToken); activeUserId = permissions.getUserId(); } - Map storedUserData = new LinkedHashMap<>(); + // we don't want to store sensitive information in the database, + // but we can send it once to the user in sync auth + Map dbStoredUserData = new LinkedHashMap<>(); + Map sentToUserAuthData = new LinkedHashMap<>(); + SMTeam[] allTeams = null; SMAuthProviderCustomConfiguration providerConfig = null; String detectedAuthRole = null; - Map userAuthData = new LinkedHashMap<>(); for (SMAuthConfigurationReference authConfiguration : authProviderIds) { String authProviderId = authConfiguration.getAuthProviderId(); WebAuthProviderDescriptor authProvider = getAuthProvider(authProviderId); @@ -1768,11 +2315,14 @@ private SMAuthInfo finishAuthentication( } } - userAuthData.putAll((Map) authInfo.getAuthData().get(authConfiguration)); + Map providerAuthData = new LinkedHashMap<>( + (Map) authInfo.getAuthData().get(authConfiguration) + ); + if (isMainAuthSession) { SMAutoAssign autoAssign = - getAutoAssignUserData(authProvider, providerConfig, userAuthData, finishAuthMonitor); + getAutoAssignUserData(authProvider, providerConfig, providerAuthData, finishAuthMonitor); if (autoAssign != null) { detectedAuthRole = autoAssign.getAuthRole(); } @@ -1780,7 +2330,7 @@ private SMAuthInfo finishAuthentication( var userIdFromCreds = findOrCreateExternalUserByCredentials( authProvider, authAttemptSessionInfo.getSessionParams(), - userAuthData, + providerAuthData, finishAuthMonitor, activeUserId, activeUserId == null, @@ -1790,8 +2340,8 @@ private SMAuthInfo finishAuthentication( if (userIdFromCreds == null) { var error = "Invalid user credentials"; - updateAuthStatus(authId, SMAuthStatus.ERROR, storedUserData, error); - return SMAuthInfo.error(authId, error); + updateAuthStatus(authId, SMAuthStatus.ERROR, dbStoredUserData, error, null); + return SMAuthInfo.error(authId, error, isMainAuthSession, null, authInfo.getAppSessionId()); } if (autoAssign != null && !CommonUtils.isEmpty(autoAssign.getExternalTeamIds())) { @@ -1804,9 +2354,25 @@ private SMAuthInfo finishAuthentication( if (activeUserId == null) { activeUserId = userIdFromCreds; } + if (autoAssign != null && CommonUtils.isNotEmpty(autoAssign.getAuthRoleAssignReason())) { + log.info(activeUserId + " authenticated with role " + autoAssign.getAuthRole() + ", reason: " + autoAssign.getAuthRoleAssignReason()); + } + } + dbStoredUserData.put( + authConfiguration, + isAsyncAuth + ? providerAuthData + : filterSecuredUserData(providerAuthData, getAuthProvider(authProviderId)) + ); + sentToUserAuthData.put( + authConfiguration, + isAsyncAuth || (authProvider.getInstance() instanceof SMAuthProviderExternal) + ? providerAuthData + : filterSecuredUserData(providerAuthData, getAuthProvider(authProviderId)) + ); + if (authProvider.getInstance() instanceof SMAuthProviderExternal authProviderExternal) { + authProviderExternal.postAuthentication(); } - storedUserData.put(authConfiguration, - saveSecuredCreds ? userAuthData : filterSecuredUserData(userAuthData, getAuthProvider(authProviderId))); } String tokenAuthRole = updateUserAuthRoleIfNeeded(activeUserId, detectedAuthRole); @@ -1826,7 +2392,17 @@ private SMAuthInfo finishAuthentication( smSessionId = authAttemptSessionInfo.getSmSessionId(); } - smTokens = generateNewSessionToken(smSessionId, activeUserId, tokenAuthRole, dbCon); + if (forceSessionsLogout && CommonUtils.isNotEmpty(activeUserId) && isMainAuthSession) { + killAllExistsUserSessions(activeUserId); + } + smTokens = generateNewSessionToken( + smSessionId, + activeUserId, + tokenAuthRole, + dbCon, + authAttemptSessionInfo.isServiceAuth() + ); + permissions = new SMAuthPermissions( activeUserId, smSessionId, getUserPermissions(activeUserId, tokenAuthRole) ); @@ -1834,12 +2410,12 @@ activeUserId, smSessionId, getUserPermissions(activeUserId, tokenAuthRole) } } catch (SQLException e) { var error = "Error during token generation"; - updateAuthStatus(authId, SMAuthStatus.ERROR, storedUserData, error); + updateAuthStatus(authId, SMAuthStatus.ERROR, dbStoredUserData, error, null); throw new SMException(error, e); } } - var authStatus = forceExpireAuthAfterSuccess ? SMAuthStatus.EXPIRED : SMAuthStatus.SUCCESS; - updateAuthStatus(authId, authStatus, storedUserData, null, permissions.getSessionId()); + var authStatus = isSyncAuth ? SMAuthStatus.EXPIRED : SMAuthStatus.SUCCESS; + updateAuthStatus(authId, authStatus, dbStoredUserData, null, permissions.getSessionId(), null); if (isMainAuthSession) { return SMAuthInfo.successMainSession( @@ -1848,14 +2424,16 @@ activeUserId, smSessionId, getUserPermissions(activeUserId, tokenAuthRole) //refresh token must be sent only from main session smTokens.getSmRefreshToken(), permissions, - authInfo.getAuthData(), - tokenAuthRole + sentToUserAuthData, + tokenAuthRole, + authInfo.getAppSessionId() ); } else { return SMAuthInfo.successChildSession( authId, permissions, - authInfo.getAuthData() + sentToUserAuthData, + authInfo.getAppSessionId() ); } } @@ -1865,24 +2443,30 @@ private void autoUpdateUserTeams( SMAutoAssign autoAssign, String userId, SMTeam[] allTeams - ) throws DBCException { - if (!(authProvider.getInstance() instanceof SMAuthProviderAssigner)) { + ) throws DBException { + if (!(authProvider.getInstance() instanceof SMAuthProviderAssigner authProviderAssigner)) { return; } - SMAuthProviderAssigner authProviderAssigner = (SMAuthProviderAssigner) authProvider.getInstance(); String externalTeamIdMetadataFieldName = authProviderAssigner.getExternalTeamIdMetadataFieldName(); if (!CommonUtils.isEmpty(externalTeamIdMetadataFieldName)) { String[] newTeamIds = autoAssign.getExternalTeamIds() .stream() - .map(externalTeamId -> findTeamByExternalTeamId( + .flatMap(externalTeamId -> findTeamByExternalTeamId( allTeams, externalTeamIdMetadataFieldName, externalTeamId - )) - .filter(Objects::nonNull) + ).stream()) .map(SMTeam::getTeamId) .toArray(String[]::new); + SMUserTeam[] oldUserTeams = getUserTeams(userId); + Set oldUserTeamIdSet = Arrays.stream(oldUserTeams).map(SMTeam::getTeamId).collect(Collectors.toSet()); + oldUserTeamIdSet.remove(getDefaultUserTeam()); + Set newUserTeamIdSet = Arrays.stream(newTeamIds).collect(Collectors.toSet()); + if (oldUserTeamIdSet.equals(newUserTeamIdSet)) { + //do not need to update teams and send events + return; + } if (!ArrayUtils.isEmpty(newTeamIds)) { setUserTeams( userId, @@ -1907,24 +2491,23 @@ private SMAutoAssign getAutoAssignUserData( return ((SMAuthProviderAssigner) authProviderInstance).detectAutoAssignments(monitor, providerConfig, userData); } - @Nullable - private SMTeam findTeamByExternalTeamId(SMTeam[] allTeams, String externalGroupParameterName, String groupId) { + @NotNull + private List findTeamByExternalTeamId(SMTeam[] allTeams, String externalGroupParameterName, String groupId) { + List result = new ArrayList<>(); for (SMTeam team : allTeams) { String teamGroupId = team.getMetaParameters().get(externalGroupParameterName); if (CommonUtils.equalObjects(teamGroupId, groupId)) { - return team; + result.add(team); } } - return null; + return result; } private String readProviderConfigId(String authAttemptId, String authProviderId) throws DBException { try (Connection dbCon = database.openConnection()) { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT AUTH_PROVIDER_CONFIGURATION_ID " + - "FROM {table_prefix}CB_AUTH_ATTEMPT_INFO " - + "WHERE AUTH_ID=? AND AUTH_PROVIDER_ID=?") + "SELECT AUTH_PROVIDER_CONFIGURATION_ID FROM {table_prefix}CB_AUTH_ATTEMPT_INFO WHERE AUTH_ID=? AND AUTH_PROVIDER_ID=?" )) { dbStat.setString(1, authAttemptId); dbStat.setString(2, authProviderId); @@ -1946,7 +2529,7 @@ private String readProviderConfigId(String authAttemptId, String authProviderId) protected String readUserAuthRole(String userId) throws DBException { try (Connection dbCon = database.openConnection()) { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT DEFAULT_AUTH_ROLE FROM {table_prefix}CB_USER WHERE USER_ID=?") + "SELECT DEFAULT_AUTH_ROLE FROM {table_prefix}CB_USER WHERE USER_ID=?" )) { dbStat.setString(1, userId); try (ResultSet dbResult = dbStat.executeQuery()) { @@ -1977,8 +2560,9 @@ protected String updateUserAuthRoleIfNeeded(@Nullable String userId, @Nullable S private AuthAttemptSessionInfo readAuthAttemptSessionInfo(@NotNull String authId) throws DBException { try (Connection dbCon = database.openConnection()) { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT APP_SESSION_ID,SESSION_TYPE,APP_SESSION_STATE,SESSION_ID FROM " + - "{table_prefix}CB_AUTH_ATTEMPT WHERE AUTH_ID=?") + """ + SELECT APP_SESSION_ID,SESSION_TYPE,APP_SESSION_STATE,SESSION_ID,IS_MAIN_AUTH,IS_SERVICE_AUTH \ + FROM {table_prefix}CB_AUTH_ATTEMPT WHERE AUTH_ID=?""" )) { dbStat.setString(1, authId); try (ResultSet dbResult = dbStat.executeQuery()) { @@ -1991,8 +2575,15 @@ private AuthAttemptSessionInfo readAuthAttemptSessionInfo(@NotNull String authId dbResult.getString(3), MAP_STRING_OBJECT_TYPE ); String smSessionId = dbResult.getString(4); - - return new AuthAttemptSessionInfo(appSessionId, smSessionId, sessionType, sessionParams); + boolean isMainAuth = stringToBoolean(dbResult.getString(5)); + boolean isServiceAuth = stringToBoolean(dbResult.getString(6)); + + return new AuthAttemptSessionInfo( + appSessionId, + smSessionId, + sessionType, + sessionParams, isMainAuth, isServiceAuth + ); } } } catch (SQLException e) { @@ -2007,12 +2598,12 @@ private String findOrCreateExternalUserByCredentials( @NotNull DBRProgressMonitor progressMonitor, @Nullable String activeUserId, boolean createNewUserIfNotExist, - String authRole, + @Nullable String authRole, SMAuthProviderCustomConfiguration providerConfig ) throws DBException { SMAuthProvider smAuthProviderInstance = authProvider.getInstance(); - String userId = findUserByCredentials(authProvider, userCredentials); + String userId = findUserByCredentials(authProvider, userCredentials, true); String userIdFromCredentials; try { userIdFromCredentials = smAuthProviderInstance.validateLocalAuth(progressMonitor, this, providerConfig, userCredentials, null); @@ -2030,10 +2621,12 @@ private String findOrCreateExternalUserByCredentials( return null; } - userId = userIdFromCredentials; + userId = authProvider.isCaseInsensitive() ? userIdFromCredentials.toLowerCase() : userIdFromCredentials; if (!isSubjectExists(userId)) { - createUser(userId, - Map.of(), + log.debug("Create user: " + userId); + validateAndCreateUser( + userId, + (Map) userCredentials.get(SMStandardMeta.KEY_META_PARAMS), true, resolveUserAuthRole(null, authRole) ); @@ -2043,6 +2636,10 @@ private String findOrCreateExternalUserByCredentials( userId = userIdFromCredentials; } if (authProvider.isTrusted()) { + Object reverseProxyUserRole = sessionParameters.get(SMConstants.SESSION_PARAM_TRUSTED_USER_ROLE); + if (reverseProxyUserRole instanceof String rpAuthRole) { + setUserAuthRole(userId, rpAuthRole); + } Object reverseProxyUserTeams = sessionParameters.get(SMConstants.SESSION_PARAM_TRUSTED_USER_TEAMS); if (reverseProxyUserTeams instanceof List) { setUserTeams(userId, ((List) reverseProxyUserTeams).stream().map(Object::toString).toArray(String[]::new), userId); @@ -2063,23 +2660,16 @@ protected SMTokens generateNewSessionToken( @NotNull String smSessionId, @Nullable String userId, @Nullable String authRole, - @NotNull Connection dbCon + @NotNull Connection dbCon, + boolean isServiceToken ) throws SQLException, DBException { JDBCUtils.executeStatement( - dbCon, database.normalizeTableNames("DELETE FROM {table_prefix}CB_AUTH_TOKEN WHERE SESSION_ID=?"), smSessionId); - return generateNewSessionTokens(smSessionId, userId, authRole, dbCon); - } - - private SMTokens generateNewSessionTokens( - @NotNull String smSessionId, - @Nullable String userId, - @Nullable String authRole, - @NotNull Connection dbCon - ) throws SQLException { - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_AUTH_TOKEN" + - "(TOKEN_ID,SESSION_ID,USER_ID,AUTH_ROLE,EXPIRATION_TIME,REFRESH_TOKEN_ID,REFRESH_TOKEN_EXPIRATION_TIME) " + - "VALUES(?,?,?,?,?,?,?)"))) { + dbCon, "DELETE FROM {table_prefix}CB_AUTH_TOKEN WHERE SESSION_ID=?", smSessionId); + try ( + PreparedStatement dbStat = dbCon.prepareStatement("INSERT INTO {table_prefix}CB_AUTH_TOKEN" + + "(TOKEN_ID,SESSION_ID,USER_ID,AUTH_ROLE,EXPIRATION_TIME,REFRESH_TOKEN_ID,REFRESH_TOKEN_EXPIRATION_TIME,IS_SERVICE) " + + "VALUES(?,?,?,?,?,?,?,?)") + ) { String smAccessToken = SecurityUtils.generatePassword(32); dbStat.setString(1, smAccessToken); @@ -2093,12 +2683,81 @@ private SMTokens generateNewSessionTokens( dbStat.setString(6, smRefreshToken); var refreshTokenExpirationTime = Timestamp.valueOf(LocalDateTime.now().plusMinutes(smConfig.getRefreshTokenTtl())); dbStat.setTimestamp(7, refreshTokenExpirationTime); + dbStat.setString(8, booleanToString(isServiceToken)); dbStat.execute(); return new SMTokens(smAccessToken, smRefreshToken); } } + protected void killAllExistsUserSessions( + @NotNull String userId + ) throws SQLException, DBException { + LocalDateTime currentTime = LocalDateTime.now(); + List smSessionsId = findActiveUserSessions(userId, currentTime, false) + .stream() + .map(SMActiveSession::sessionId) + .collect(Collectors.toList()); + if (!smSessionsId.isEmpty()) { + deleteSessionsTokens(smSessionsId); + application.getEventController().addEvent(new WSUserCloseSessionsEvent(smSessionsId, getSmSessionId(), userId)); + } + } + + /** + * Count active user's sm sessions + */ + @NotNull + public List findActiveUserSessions( + @NotNull String userId, + @NotNull LocalDateTime currentTime, + boolean isService + ) throws DBException { + var activeSessions = new ArrayList(); + try (var dbCon = database.openConnection()) { + try ( + PreparedStatement dbStat = dbCon.prepareStatement( + "SELECT DISTINCT CAT.SESSION_ID, CAT.EXPIRATION_TIME " + + "FROM {table_prefix}CB_AUTH_TOKEN CAT " + + "JOIN {table_prefix}CB_AUTH_ATTEMPT CAA ON CAA.SESSION_ID = CAT.SESSION_ID WHERE " + + "CAT.USER_ID=? AND CAA.AUTH_STATUS=? AND CAT.EXPIRATION_TIME>? AND CAT.IS_SERVICE=? " + + "ORDER BY CAT.EXPIRATION_TIME" + ) + ) { + dbStat.setString(1, userId); + //count only tokens actually used by users + dbStat.setString(2, SMAuthStatus.EXPIRED.name()); + dbStat.setTimestamp(3, Timestamp.valueOf(currentTime)); + dbStat.setString(4, booleanToString(isService)); + try (ResultSet dbResult = dbStat.executeQuery()) { + while (dbResult.next()) { + var sessionId = dbResult.getString(1); + var expirationTime = dbResult.getTimestamp(2); + activeSessions.add(new SMActiveSession(sessionId, expirationTime)); + } + } + return activeSessions; + } + } catch (SQLException e) { + throw new DBException("Error counting active user's session", e); + } + } + + private void deleteSessionsTokens(@NotNull List sessionsId) throws DBException { + try (var dbCon = database.openConnection()) { + try (PreparedStatement dbStat = dbCon.prepareStatement( + "DELETE FROM {table_prefix}CB_AUTH_TOKEN WHERE SESSION_ID = ?") + ) { + for (String sessionId : sessionsId) { + dbStat.setString(1, sessionId); + dbStat.executeUpdate(); + } + } + } catch (SQLException e) { + throw new DBException("Error delete active user's session", e); + } + } + @Override public SMAuthPermissions getTokenPermissions() throws DBException { SMCredentials activeUserCredentials = credentialsProvider.getActiveUserCredentials(); @@ -2115,18 +2774,19 @@ private SMAuthPermissions getTokenPermissions(@NotNull String token) throws DBEx String authRole; try (Connection dbCon = database.openConnection(); PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT USER_ID, EXPIRATION_TIME, SESSION_ID, AUTH_ROLE FROM {table_prefix}CB_AUTH_TOKEN " + - "WHERE TOKEN_ID=?")); + """ + SELECT USER_ID, EXPIRATION_TIME, SESSION_ID, AUTH_ROLE FROM {table_prefix}CB_AUTH_TOKEN \ + WHERE TOKEN_ID=?""") ) { dbStat.setString(1, token); try (var dbResult = dbStat.executeQuery()) { if (!dbResult.next()) { - throw new SMException("Invalid token"); + throw new SMException("Error reading permissions: input token not recognized."); } userId = dbResult.getString(1); var expiredDate = dbResult.getTimestamp(2); if (application.isMultiNode() && isTokenExpired(expiredDate)) { - throw new SMAccessTokenExpiredException("Token expired"); + throw new SMAccessTokenExpiredException("Error reading permissions: token has expired"); } sessionId = dbResult.getString(3); authRole = dbResult.getString(4); @@ -2140,17 +2800,14 @@ private SMAuthPermissions getTokenPermissions(@NotNull String token) throws DBEx @Override public SMAuthProviderDescriptor[] getAvailableAuthProviders() throws DBException { - if (!(application.getAppConfiguration() instanceof WebAuthConfiguration)) { - throw new DBException("Web application doesn't support external authentication"); - } - WebAuthConfiguration appConfiguration = (WebAuthConfiguration) application.getAppConfiguration(); + ServletAuthConfiguration appConfiguration = application.getAuthConfiguration(); Set customConfigurations = appConfiguration.getAuthCustomConfigurations(); List providers = WebAuthProviderRegistry.getInstance().getAuthProviders().stream() .filter(ap -> - !ap.isTrusted() && + !ap.isTrusted() && !ap.isAuthHidden() && appConfiguration.isAuthProviderEnabled(ap.getId()) && (!ap.isConfigurable() || hasProviderConfiguration(ap, customConfigurations))) - .map(WebAuthProviderDescriptor::createDescriptorBean).collect(Collectors.toList()); + .map(WebAuthProviderDescriptor::createDescriptorBean).toList(); if (!CommonUtils.isEmpty(customConfigurations)) { // Attach custom configs to providers @@ -2182,9 +2839,10 @@ public void updateSession(@NotNull String sessionId, @NotNull Map objectIds, + @NotNull SMObjectType objectType, + @NotNull Set subjectIds, + @NotNull Set permissions, + @NotNull String grantor + ) throws DBException { + if (CommonUtils.isEmpty(objectIds) || CommonUtils.isEmpty(subjectIds) || CommonUtils.isEmpty(permissions)) { + return; + } + Set filteredSubjects = getFilteredSubjects(subjectIds); + try (Connection dbCon = database.openConnection(); + JDBCTransaction txn = new JDBCTransaction(dbCon); + PreparedStatement dbStat = dbCon.prepareStatement( + """ + INSERT INTO {table_prefix}CB_OBJECT_PERMISSIONS\ + (OBJECT_ID,OBJECT_TYPE,GRANT_TIME,GRANTED_BY,SUBJECT_ID,PERMISSION) \ + VALUES(?,?,?,?,?,?)""") + ) { + for (String objectId : objectIds) { + dbStat.setString(1, objectId); + dbStat.setString(2, objectType.name()); + dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); + dbStat.setString(4, grantor); + for (String subjectId : subjectIds) { + if (!filteredSubjects.contains(subjectId)) { + log.error("Subject '" + subjectId + "' is not found in database"); + continue; + } + dbStat.setString(5, subjectId); + for (String permission : permissions) { + dbStat.setString(6, permission); + dbStat.execute(); + } + } + txn.commit(); + } + addObjectPermissionsUpdateEvent(objectIds, objectType); + } catch (SQLException e) { + throw new DBCException("Error granting object permissions", e); + } + } + + @Override + public void deleteObjectPermissions( + @NotNull Set objectIds, + @NotNull SMObjectType objectType, + @NotNull Set subjectIds, + @NotNull Set permissions + ) throws DBException { + if (CommonUtils.isEmpty(objectIds) || CommonUtils.isEmpty(subjectIds) || CommonUtils.isEmpty(permissions)) { + return; + } + String sql = "DELETE FROM {table_prefix}CB_OBJECT_PERMISSIONS WHERE " + + "OBJECT_TYPE=?" + + " AND " + + "OBJECT_ID IN (" + SQLUtils.generateParamList(objectIds.size()) + ")" + + " AND " + + "SUBJECT_ID IN (" + SQLUtils.generateParamList(subjectIds.size()) + ")" + + " AND " + + "PERMISSION IN (" + SQLUtils.generateParamList(permissions.size()) + ")"; + + try ( + Connection dbCon = database.openConnection(); + PreparedStatement dbStat = dbCon.prepareStatement(sql) + ) { + int index = 1; + dbStat.setString(index++, objectType.name()); + for (String objectId : objectIds) { + dbStat.setString(index++, objectId); + } + for (String subjectId : subjectIds) { + dbStat.setString(index++, subjectId); + } + for (String permission : permissions) { + dbStat.setString(index++, permission); + } + dbStat.execute(); + addObjectPermissionsDeleteEvent(objectIds, objectType); + } catch (SQLException e) { + throw new DBCException("Error granting object permissions", e); + } + } + - private void addSubjectPermissionsUpdateEvent(@NotNull String subjectId, @Nullable SMSubjectType subjectType) { + protected void addSubjectPermissionsUpdateEvent(@NotNull String subjectId, @Nullable SMSubjectType subjectType) { if (subjectType == null) { subjectType = getSubjectType(subjectId); } @@ -2274,13 +3017,7 @@ private void addSubjectPermissionsUpdateEvent(@NotNull String subjectId, @Nullab log.error("Subject type is not found for subject '" + subjectId + "'"); return; } - var event = WSSubjectPermissionEvent.update( - getSmSessionId(), - getUserId(), - subjectType, - subjectId - ); - application.getEventController().addEvent(event); + WebEventUtils.addSubjectPermissionsUpdateEvent(subjectId, subjectType, getSmSessionId(), getUserId()); } private void addObjectPermissionsUpdateEvent(@NotNull Set objectIds, @NotNull SMObjectType objectType) { @@ -2295,11 +3032,23 @@ private void addObjectPermissionsUpdateEvent(@NotNull Set objectIds, @No } } + private void addObjectPermissionsDeleteEvent(@NotNull Set objectIds, @NotNull SMObjectType objectType) { + for (var objectId : objectIds) { + var event = WSObjectPermissionEvent.delete( + getSmSessionId(), + getUserId(), + objectType, + objectId + ); + application.getEventController().addEvent(event); + } + } + @Override public void deleteAllObjectPermissions(@NotNull String objectId, @NotNull SMObjectType objectType) throws DBException { try (Connection dbCon = database.openConnection()) { JDBCUtils.executeStatement(dbCon, - database.normalizeTableNames("DELETE FROM {table_prefix}CB_OBJECT_PERMISSIONS WHERE OBJECT_TYPE=? AND OBJECT_ID=?"), + "DELETE FROM {table_prefix}CB_OBJECT_PERMISSIONS WHERE OBJECT_TYPE=? AND OBJECT_ID=?", objectType.name(), objectId ); @@ -2309,11 +3058,47 @@ public void deleteAllObjectPermissions(@NotNull String objectId, @NotNull SMObje } } + @Override + public boolean hasAccessToUsers(@NotNull String teamRole, @NotNull Set userIds) throws DBException { + if (CommonUtils.isEmpty(userIds)) { + return true; + } + String currentUserId = getUserIdOrThrow(); + var currentPermissions = getUserPermissions(currentUserId); + if (currentPermissions.contains(DBWConstants.PERMISSION_ADMIN)) { + return true; + } + + String sql = "SELECT COUNT(DISTINCT UT.USER_ID) FROM {table_prefix}CB_USER_TEAM UT " + + "WHERE TEAM_ID IN (SELECT TEAM_ID FROM {table_prefix}CB_USER_TEAM WHERE USER_ID = ? and TEAM_ROLE = ?) " + + "AND UT.USER_ID IN(" + SQLUtils.generateParamList(userIds.size()) + ")"; + try (var dbCon = database.openConnection(); + var dbStat = dbCon.prepareStatement(sql) + ) { + dbStat.setString(1, currentUserId); + dbStat.setString(2, teamRole); + int index = 3; + for (String userId : userIds) { + dbStat.setString(index++, userId); + } + try (ResultSet dbResult = dbStat.executeQuery()) { + if (dbResult.next()) { + int matchesUsersCount = dbResult.getInt(1); + return matchesUsersCount == userIds.size(); + } else { + return false; + } + } + } catch (SQLException e) { + throw new DBCException("Error validating user access", e); + } + } + @Override public void deleteAllSubjectObjectPermissions(@NotNull String subjectId, @NotNull SMObjectType objectType) throws DBException { try (Connection dbCon = database.openConnection()) { JDBCUtils.executeStatement(dbCon, - database.normalizeTableNames("DELETE FROM {table_prefix}CB_OBJECT_PERMISSIONS WHERE OBJECT_TYPE=? AND SUBJECT_ID=?"), + "DELETE FROM {table_prefix}CB_OBJECT_PERMISSIONS WHERE OBJECT_TYPE=? AND SUBJECT_ID=?", objectType.name(), subjectId ); @@ -2346,7 +3131,7 @@ public List getAllAvailableObjectsPermissions(@NotNull SMOb sqlBuilder.append("WHERE SUBJECT_ID IN ("); appendStringParameters(sqlBuilder, allSubjects); sqlBuilder.append(") AND OBJECT_TYPE=?"); - try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(sqlBuilder.toString()))) { + try (PreparedStatement dbStat = dbCon.prepareStatement(sqlBuilder.toString())) { dbStat.setString(1, objectType.name()); var permissionsByObjectId = new LinkedHashMap>(); @@ -2382,7 +3167,7 @@ public SMObjectPermissions getObjectPermissions( appendStringParameters(sqlBuilder, allSubjects); sqlBuilder.append(") AND OBJECT_TYPE=? AND OBJECT_ID=?"); - try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(sqlBuilder.toString()))) { + try (PreparedStatement dbStat = dbCon.prepareStatement(sqlBuilder.toString())) { dbStat.setString(1, objectType.name()); dbStat.setString(2, objectId); @@ -2408,13 +3193,13 @@ public List getObjectPermissionGrants( ) throws DBException { var grantedPermissionsBySubjectId = new HashMap(); try (Connection dbCon = database.openConnection()) { - try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames( - "SELECT OP.SUBJECT_ID,S.SUBJECT_TYPE, OP.PERMISSION\n" + - "FROM {table_prefix}CB_OBJECT_PERMISSIONS OP, {table_prefix}CB_AUTH_SUBJECT S\n" + - "WHERE S.SUBJECT_ID = OP.SUBJECT_ID AND OP.OBJECT_TYPE=? AND OP.OBJECT_ID=?"))) { + try (PreparedStatement dbStat = dbCon.prepareStatement( + """ + SELECT OP.SUBJECT_ID,S.SUBJECT_TYPE, OP.PERMISSION + FROM {table_prefix}CB_OBJECT_PERMISSIONS OP, {table_prefix}CB_AUTH_SUBJECT S + WHERE S.SUBJECT_ID = OP.SUBJECT_ID AND OP.OBJECT_TYPE=? AND OP.OBJECT_ID=?""")) { dbStat.setString(1, smObjectType.name()); dbStat.setString(2, objectId); - List result = new ArrayList<>(); try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { String subjectId = dbResult.getString(1); @@ -2447,7 +3232,7 @@ public List getSubjectObjectPermissionGrants(@NotNull .append("WHERE S.SUBJECT_ID = OP.SUBJECT_ID AND OP.SUBJECT_ID IN ("); appendStringParameters(sqlBuilder, allLinkedSubjects); sqlBuilder.append(") AND OP.OBJECT_TYPE=?"); - try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(sqlBuilder.toString()))) { + try (PreparedStatement dbStat = dbCon.prepareStatement(sqlBuilder.toString())) { dbStat.setString(1, smObjectType.name()); try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { @@ -2463,7 +3248,7 @@ public List getSubjectObjectPermissionGrants(@NotNull } return grantedPermissionsByObjectId.values().stream() .map(SMObjectPermissionsGrant.Builder::build) - .collect(Collectors.toList()); + .toList(); } } catch (SQLException e) { @@ -2487,7 +3272,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); } @@ -2497,68 +3282,55 @@ public void finishConfiguration( protected String readTokenAuthRole(String smAccessToken) throws DBException { try (Connection dbCon = database.openConnection(); - PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT AUTH_ROLE FROM {table_prefix}CB_AUTH_TOKEN WHERE TOKEN_ID=?")) + PreparedStatement dbStat = dbCon.prepareStatement("SELECT AUTH_ROLE FROM {table_prefix}CB_AUTH_TOKEN WHERE TOKEN_ID=?") ) { dbStat.setString(1, smAccessToken); try (var dbResult = dbStat.executeQuery()) { if (!dbResult.next()) { - throw new SMException("Invalid token"); + throw new SMException("Error reading subject role: input token not recognized."); } return dbResult.getString(1); } } catch (SQLException e) { - throw new DBCException("Error reading lm role in database", e); - } - } - - public void initializeMetaInformation() throws DBCException { - try (Connection dbCon = database.openConnection()) { - try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { - Set registeredProviders = new HashSet<>(); - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT PROVIDER_ID FROM {table_prefix}CB_AUTH_PROVIDER"))) { - try (ResultSet dbResult = dbStat.executeQuery()) { - while (dbResult.next()) { - registeredProviders.add(dbResult.getString(1)); - } - } - } - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_AUTH_PROVIDER(PROVIDER_ID,IS_ENABLED) VALUES(?,'Y')"))) { - for (WebAuthProviderDescriptor authProvider : WebAuthProviderRegistry.getInstance().getAuthProviders()) { - if (!registeredProviders.contains(authProvider.getId())) { - dbStat.setString(1, authProvider.getId()); - dbStat.executeUpdate(); - log.debug("Auth provider '" + authProvider.getId() + "' registered"); - } - } - } - txn.commit(); - } - } catch (SQLException e) { - throw new DBCException("Error initializing security manager meta info", e); + throw new DBCException("Error reading subject role in database", e); } } - private void createAuthSubject(Connection dbCon, String subjectId, String subjectType) throws SQLException { + private void createAuthSubject( + Connection dbCon, + String subjectId, + SMSubjectType subjectType, + boolean secretStorage + ) throws SQLException { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_AUTH_SUBJECT(SUBJECT_ID,SUBJECT_TYPE) VALUES(?,?)"))) { + "INSERT INTO {table_prefix}CB_AUTH_SUBJECT(SUBJECT_ID,SUBJECT_TYPE,IS_SECRET_STORAGE) " + + "VALUES (?,?,?)")) { dbStat.setString(1, subjectId); - dbStat.setString(2, subjectType); + dbStat.setString(2, subjectType.getCode()); + dbStat.setString(3, booleanToString(secretStorage)); dbStat.execute(); } } + public static String booleanToString(boolean value) { + return value ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE; + } + + public static boolean stringToBoolean(@Nullable String value) { + return CHAR_BOOL_TRUE.equals(value); + } + private void deleteAuthSubject(Connection dbCon, String subjectId) throws SQLException { try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("DELETE FROM {table_prefix}CB_AUTH_SUBJECT WHERE SUBJECT_ID=?"))) { + "DELETE FROM {table_prefix}CB_AUTH_SUBJECT WHERE SUBJECT_ID=?") + ) { dbStat.setString(1, subjectId); dbStat.execute(); } } - private WebAuthProviderDescriptor getAuthProvider(String authProviderId) throws DBCException { + @NotNull + protected WebAuthProviderDescriptor getAuthProvider(String authProviderId) throws DBCException { WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(authProviderId); if (authProvider == null) { throw new DBCException("Auth provider not found: " + authProviderId); @@ -2566,13 +3338,13 @@ private WebAuthProviderDescriptor getAuthProvider(String authProviderId) throws return authProvider; } - private String buildRedirectLink(String originalLink, String authId) { + private String buildRedirectLink(@NotNull String originalLink, @NotNull String authId) { return originalLink + "?authId=" + authId; } @NotNull - private String getUserIdOrThrow() throws SMException { + protected String getUserIdOrThrow() throws SMException { String userId = getUserIdOrNull(); if (userId == null) { throw new SMException("User not authenticated"); @@ -2581,7 +3353,7 @@ private String getUserIdOrThrow() throws SMException { } @Nullable - private String getUserIdOrNull() { + protected String getUserIdOrNull() { SMCredentials activeUserCredentials = credentialsProvider.getActiveUserCredentials(); if (activeUserCredentials == null || activeUserCredentials.getUserId() == null) { return null; @@ -2589,21 +3361,31 @@ private String getUserIdOrNull() { return activeUserCredentials.getUserId(); } - private boolean isProviderEnabled(@NotNull String providerId) { - WebAuthConfiguration appConfiguration = application.getAuthConfiguration(); - return appConfiguration.isAuthProviderEnabled(providerId); + private boolean isProviderDisabled(@NotNull String providerId, @Nullable String authConfigurationId) { + ServletAuthConfiguration appConfiguration = application.getAuthConfiguration(); + if (!appConfiguration.isAuthProviderEnabled(providerId)) { + return true; + } + if (authConfigurationId != null) { + SMAuthProviderCustomConfiguration configuration = + appConfiguration.getAuthProviderConfiguration(authConfigurationId); + return configuration == null || configuration.isDisabled(); + } + return false; } public void clearOldAuthAttemptInfo() throws DBException { try (Connection dbCon = database.openConnection()) { JDBCUtils.executeStatement(dbCon, - database.normalizeTableNames("DELETE FROM {table_prefix}CB_AUTH_ATTEMPT_INFO AAI " + + "DELETE FROM {table_prefix}CB_AUTH_ATTEMPT_INFO " + "WHERE EXISTS " + "(SELECT 1 FROM {table_prefix}CB_AUTH_ATTEMPT AA " + "LEFT JOIN {table_prefix}CB_AUTH_TOKEN CAT ON AA.SESSION_ID = CAT.SESSION_ID " + - "WHERE (CAT.REFRESH_TOKEN_EXPIRATION_TIME < NOW() OR CAT.EXPIRATION_TIME IS NULL) " + - "AND AA.AUTH_ID=AAI.AUTH_ID AND AUTH_STATUS='" + SMAuthStatus.EXPIRED + "') " + - "AND CREATE_TIME getFilteredSubjects(Set allSubjects) { try (Connection dbCon = database.openConnection()) { Set result = new HashSet<>(); - var sqlBuilder = new StringBuilder("SELECT SUBJECT_ID FROM {table_prefix}CB_AUTH_SUBJECT U ") - .append("WHERE SUBJECT_ID IN (") - .append(SQLUtils.generateParamList(allSubjects.size())) - .append(")"); - try (var dbStat = dbCon.prepareStatement(database.normalizeTableNames(sqlBuilder.toString()))) { + String sqlBuilder = + "SELECT SUBJECT_ID FROM {table_prefix}CB_AUTH_SUBJECT U " + + "WHERE SUBJECT_ID IN (" + SQLUtils.generateParamList(allSubjects.size()) + ")"; + try (var dbStat = dbCon.prepareStatement(sqlBuilder)) { int parameterIndex = 1; for (String subjectId : allSubjects) { dbStat.setString(parameterIndex++, subjectId); @@ -2636,19 +3417,9 @@ public Set getFilteredSubjects(Set allSubjects) { } } - private SMSubjectType getSubjectType(@NotNull String subjectId) { + protected SMSubjectType getSubjectType(@NotNull String subjectId) { try (Connection dbCon = database.openConnection()) { - Set result = new HashSet<>(); - String sqlBuilder = "SELECT SUBJECT_TYPE FROM {table_prefix}CB_AUTH_SUBJECT U WHERE SUBJECT_ID = ?"; - try (var dbStat = dbCon.prepareStatement(database.normalizeTableNames(sqlBuilder))) { - dbStat.setString(1, subjectId); - try (ResultSet dbResult = dbStat.executeQuery()) { - if (dbResult.next()) { - return SMSubjectType.fromCode(dbResult.getString(1)); - } - } - } - return null; + return CBAuthSubjectRepo.getInstance().getSubjectType(dbCon, subjectId); } catch (SQLException e) { log.error("Error getting all subject ids from database", e); return null; @@ -2666,4 +3437,15 @@ private String getUserId() { var credentials = credentialsProvider.getActiveUserCredentials(); return credentials == null ? null : credentials.getUserId(); } + + @NotNull + private String getDefaultUserTeam() { + return application.getAppConfiguration().getDefaultUserTeam(); + } + + @NotNull + @Override + public DBPConnectionInformation getInternalDatabaseInformation() { + return database.getMetaDataInfo(); + } } 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..9208e4b281 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,22 +16,21 @@ */ package io.cloudbeaver.service.security; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.InstanceCreator; -import io.cloudbeaver.model.app.WebAuthApplication; +import io.cloudbeaver.auth.NoAuthCredentialsProvider; +import io.cloudbeaver.model.app.ServletApplication; +import io.cloudbeaver.model.app.ServletAuthApplication; +import io.cloudbeaver.model.config.SMControllerConfiguration; +import io.cloudbeaver.model.config.WebDatabaseConfig; import io.cloudbeaver.service.security.db.CBDatabase; -import io.cloudbeaver.service.security.db.CBDatabaseConfig; import io.cloudbeaver.service.security.internal.ClearAuthAttemptInfoJob; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; -import java.util.Map; - /** * Embedded Security Controller Factory */ -public class EmbeddedSecurityControllerFactory { +public class EmbeddedSecurityControllerFactory { private static volatile CBDatabase DB_INSTANCE; public static CBDatabase getDbInstance() { @@ -41,56 +40,72 @@ public static CBDatabase getDbInstance() { /** * Create new security controller instance with custom configuration */ - public CBEmbeddedSecurityController createSecurityService( - WebAuthApplication application, - Map databaseConfig, + public CBEmbeddedSecurityController createSecurityService( + T application, + WebDatabaseConfig databaseConfig, SMCredentialsProvider credentialsProvider, SMControllerConfiguration smConfig ) throws DBException { - boolean initDb = false; + boolean initialization = false; if (DB_INSTANCE == null) { + initialization = true; synchronized (EmbeddedSecurityControllerFactory.class) { if (DB_INSTANCE == null) { - initDatabase(application, databaseConfig); - initDb = true; + DB_INSTANCE = createAndInitDatabaseInstance( + application, + databaseConfig, + smConfig + ); } } - } - var securityController = createEmbeddedSecurityController( - application, DB_INSTANCE, credentialsProvider, smConfig - ); - if (initDb) { - //FIXME circular dependency - DB_INSTANCE.setAdminSecurityController(securityController); - DB_INSTANCE.initialize(); - securityController.initializeMetaInformation(); + if (application.isLicenseRequired()) { // delete expired auth info job in enterprise products - new ClearAuthAttemptInfoJob(securityController).schedule(); + new ClearAuthAttemptInfoJob(createEmbeddedSecurityController( + application, DB_INSTANCE, new NoAuthCredentialsProvider(), smConfig + )).schedule(); } } - return securityController; + var controller = createEmbeddedSecurityController( + application, DB_INSTANCE, credentialsProvider, smConfig + ); + if (initialization) { + controller.initialize(); + } + return controller; } - private synchronized void initDatabase(WebAuthApplication application, Map databaseConfig) { - CBDatabaseConfig databaseConfiguration = new CBDatabaseConfig(); - InstanceCreator dbConfigCreator = type -> databaseConfiguration; - InstanceCreator dbPoolConfigCreator = type -> databaseConfiguration.getPool(); - Gson gson = new GsonBuilder() - .registerTypeAdapter(CBDatabaseConfig.class, dbConfigCreator) - .registerTypeAdapter(CBDatabaseConfig.Pool.class, dbPoolConfigCreator) - .create(); - gson.fromJson(gson.toJsonTree(databaseConfig), CBDatabaseConfig.class); + protected @NotNull CBDatabase createAndInitDatabaseInstance( + @NotNull T application, + @NotNull WebDatabaseConfig databaseConfig, + @NotNull SMControllerConfiguration smConfig + ) throws DBException { + var database = makeDatabase(application, databaseConfig); + var securityController = createEmbeddedSecurityController( + application, database, new NoAuthCredentialsProvider(), smConfig + ); + //FIXME circular dependency + database.setAdminSecurityController(securityController); + try { + database.initialize(); + } catch (DBException e) { + database.shutdown(); + throw e; + } - DB_INSTANCE = new CBDatabase(application, databaseConfiguration); + return database; } - protected CBEmbeddedSecurityController createEmbeddedSecurityController( - WebAuthApplication application, + protected CBEmbeddedSecurityController createEmbeddedSecurityController( + T application, CBDatabase database, SMCredentialsProvider credentialsProvider, SMControllerConfiguration smConfig ) { - return new CBEmbeddedSecurityController(application, database, credentialsProvider, smConfig); + return new CBEmbeddedSecurityController(application, database, credentialsProvider, smConfig); + } + + protected CBDatabase makeDatabase(ServletApplication application, WebDatabaseConfig databaseConfig) { + return new CBDatabase(application, databaseConfig); } } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMActiveSession.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMActiveSession.java new file mode 100644 index 0000000000..acc4b2884f --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMActiveSession.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.service.security; + +import org.jkiss.code.NotNull; + +import java.sql.Timestamp; + +public record SMActiveSession(@NotNull String sessionId, @NotNull Timestamp expiredDate) { +} diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMControllerConfiguration.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMControllerConfiguration.java deleted file mode 100644 index 02b7971792..0000000000 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMControllerConfiguration.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.security; - -public class SMControllerConfiguration { - //in minutes - public static final int DEFAULT_ACCESS_TOKEN_TTL = 20; - public static final int DEFAULT_REFRESH_TOKEN_TTL = 4320; //72h - public static final int DEFAULT_EXPIRED_AUTH_ATTEMPT_INFO_TTL = 60; //72h - - private int accessTokenTtl = DEFAULT_ACCESS_TOKEN_TTL; - private int refreshTokenTtl = DEFAULT_REFRESH_TOKEN_TTL; - private int expiredAuthAttemptInfoTtl = DEFAULT_EXPIRED_AUTH_ATTEMPT_INFO_TTL; - - public int getAccessTokenTtl() { - return accessTokenTtl; - } - - public void setAccessTokenTtl(int accessTokenTtl) { - this.accessTokenTtl = accessTokenTtl; - } - - public int getRefreshTokenTtl() { - return refreshTokenTtl; - } - - public void setRefreshTokenTtl(int refreshTokenTtl) { - this.refreshTokenTtl = refreshTokenTtl; - } - - public int getExpiredAuthAttemptInfoTtl() { - return expiredAuthAttemptInfoTtl; - } - - public void setExpiredAuthAttemptInfoTtl(int expiredAuthAttemptInfoTtl) { - this.expiredAuthAttemptInfoTtl = expiredAuthAttemptInfoTtl; - } -} diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/bruteforce/BruteForceUtils.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/bruteforce/BruteForceUtils.java new file mode 100644 index 0000000000..9a0bb08e27 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/bruteforce/BruteForceUtils.java @@ -0,0 +1,68 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.security.bruteforce; + +import io.cloudbeaver.model.config.SMControllerConfiguration; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.auth.SMAuthStatus; +import org.jkiss.dbeaver.model.security.exception.SMException; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +public class BruteForceUtils { + + private static final Log log = Log.getLog(BruteForceUtils.class); + + public static void checkBruteforce(SMControllerConfiguration smConfig, List latestLoginAttempts) + throws DBException { + if (latestLoginAttempts.isEmpty()) { + return; + } + + var oldestLoginAttempt = latestLoginAttempts.get(latestLoginAttempts.size() - 1); + checkLoginInterval(oldestLoginAttempt.time(), smConfig.getMinimumLoginTimeout()); + + long errorsCount = latestLoginAttempts.stream() + .filter(authAttemptSessionInfo -> authAttemptSessionInfo.smAuthStatus() == SMAuthStatus.ERROR).count(); + + boolean shouldBlock = errorsCount >= smConfig.getMaxFailedLogin(); + if (shouldBlock) { + int blockPeriod = smConfig.getBlockLoginPeriod(); + LocalDateTime unblockTime = oldestLoginAttempt.time().plusSeconds(blockPeriod); + + LocalDateTime now = LocalDateTime.now(); + shouldBlock = unblockTime.isAfter(now); + + if (shouldBlock) { + log.error("User login is blocked due to exceeding the limit of incorrect password entry"); + Duration lockDuration = Duration.ofSeconds(smConfig.getBlockLoginPeriod()); + + throw new SMException("Blocked the possibility of login for this user for " + + lockDuration.minus(Duration.between(oldestLoginAttempt.time(), now)).getSeconds() + " seconds"); + } + } + } + + private static void checkLoginInterval(LocalDateTime createTime, int timeout) throws DBException { + if (createTime != null && Duration.between(createTime, LocalDateTime.now()).getSeconds() < timeout) { + throw new DBException("Too frequent authentication requests"); + } + } +} diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/bruteforce/UserLoginRecord.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/bruteforce/UserLoginRecord.java new file mode 100644 index 0000000000..3589d92ed3 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/bruteforce/UserLoginRecord.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.security.bruteforce; + +import org.jkiss.dbeaver.model.auth.SMAuthStatus; + +import java.time.LocalDateTime; + +public record UserLoginRecord(SMAuthStatus smAuthStatus, LocalDateTime time) { +} 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..a339307bcd 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 @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -18,12 +18,13 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.Strictness; import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants; -import io.cloudbeaver.model.app.WebApplication; -import io.cloudbeaver.model.session.WebAuthInfo; +import io.cloudbeaver.model.app.ServletApplication; +import io.cloudbeaver.model.config.WebDatabaseConfig; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.utils.ServletAppUtils; import org.apache.commons.dbcp2.*; import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; @@ -31,8 +32,9 @@ import org.jkiss.code.Nullable; 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.app.ApplicationRegistry; import org.jkiss.dbeaver.model.impl.jdbc.JDBCUtils; import org.jkiss.dbeaver.model.impl.jdbc.exec.JDBCTransaction; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; @@ -40,56 +42,81 @@ import org.jkiss.dbeaver.model.security.SMAdminController; import org.jkiss.dbeaver.model.security.user.SMTeam; import org.jkiss.dbeaver.model.security.user.SMUser; -import org.jkiss.dbeaver.model.sql.SQLDialect; -import org.jkiss.dbeaver.model.sql.SQLDialectSchemaController; -import org.jkiss.dbeaver.model.sql.schema.ClassLoaderScriptSource; -import org.jkiss.dbeaver.model.sql.schema.SQLSchemaManager; -import org.jkiss.dbeaver.model.sql.schema.SQLSchemaVersionManager; +import org.jkiss.dbeaver.model.sql.db.InternalDB; +import org.jkiss.dbeaver.model.sql.db.InternalProxyConnection; +import org.jkiss.dbeaver.model.sql.schema.SQLSchemaConfig; import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; import org.jkiss.dbeaver.registry.storage.H2Migrator; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.utils.GeneralUtils; import org.jkiss.dbeaver.utils.RuntimeUtils; -import org.jkiss.dbeaver.utils.SystemVariablesResolver; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; import org.jkiss.utils.SecurityUtils; import java.io.*; import java.net.InetAddress; +import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; -import java.sql.*; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.PreparedStatement; +import java.sql.SQLException; import java.util.*; import java.util.stream.Collectors; +import javax.sql.DataSource; /** * Database management */ -public class CBDatabase { +public class CBDatabase extends InternalDB { private static final Log log = Log.getLog(CBDatabase.class); - public static final String SCHEMA_CREATE_SQL_PATH = "db/cb_schema_create.sql"; - public static final String SCHEMA_UPDATE_SQL_PATH = "db/cb_schema_update_"; + private static final int CURRENT_SCHEMA_VERSION = 25; + private static final String SCHEMA_ID = "CB_CE"; - private static final int LEGACY_SCHEMA_VERSION = 1; - private static final int CURRENT_SCHEMA_VERSION = 13; + private static final SQLSchemaConfig SCHEMA_CREATE_CONFIG = new SQLSchemaConfig( + SCHEMA_ID, + "db/cb_schema_create.sql", + "db/cb_schema_update_", + CURRENT_SCHEMA_VERSION, + 0, + new CBSchemaVersionManager(CURRENT_SCHEMA_VERSION, SCHEMA_ID), + CBDatabase.class.getClassLoader(), + null + ); private static final String DEFAULT_DB_USER_NAME = "cb-data"; private static final String DEFAULT_DB_PWD_FILE = ".database-credentials.dat"; private static final String V1_DB_NAME = "cb.h2.dat"; private static final String V2_DB_NAME = "cb.h2v2.dat"; - private final WebApplication application; - private final CBDatabaseConfig databaseConfiguration; - private PoolingDataSource cbDataSource; + private final ServletApplication application; + private CBDatabaseInitialData initialData; + private transient volatile Connection exclusiveConnection; private String instanceId; private SMAdminController adminSecurityController; - public CBDatabase(WebApplication application, CBDatabaseConfig databaseConfiguration) { + public CBDatabase(@NotNull ServletApplication application, @NotNull WebDatabaseConfig databaseConfiguration) { + this(application, databaseConfiguration, Collections.emptyList()); + } + + public CBDatabase( + @NotNull ServletApplication application, + @NotNull WebDatabaseConfig databaseConfiguration, + @NotNull List sqlSchemaConfigList + ) { + super("Security Manager", databaseConfiguration, appendSchemaConfig(sqlSchemaConfigList)); this.application = application; - this.databaseConfiguration = databaseConfiguration; + SCHEMA_CREATE_CONFIG.setInitialSchemaFiller(this::fillInitialSchemaData); + } + + private static List appendSchemaConfig(List sqlSchemaConfigList) { + List sqlSchemaConfigs = new ArrayList<>(sqlSchemaConfigList); + sqlSchemaConfigs.add(0, SCHEMA_CREATE_CONFIG); + return sqlSchemaConfigs; } public void setAdminSecurityController(SMAdminController adminSecurityController) { @@ -104,139 +131,113 @@ public Connection openConnection() throws SQLException { if (exclusiveConnection != null) { return exclusiveConnection; } - return cbDataSource.getConnection(); - } - - public PoolingDataSource getConnectionPool() { - return cbDataSource; + return new InternalProxyConnection(dataSource.getConnection(), databaseConfig); } public void initialize() throws DBException { log.debug("Initiate management database"); - if (CommonUtils.isEmpty(databaseConfiguration.getDriver())) { - throw new DBException("No database driver configured for CloudBeaver database"); - } var dataSourceProviderRegistry = DataSourceProviderRegistry.getInstance(); - DBPDriver driver = dataSourceProviderRegistry.findDriver(databaseConfiguration.getDriver()); - if (driver == null) { - throw new DBException("Driver '" + databaseConfiguration.getDriver() + "' not found"); + DBPDriver driver = getDatabaseDriver(dataSourceProviderRegistry); + if (isDefaultH2Configuration(databaseConfig)) { + //force use default values even if they are explicitly specified + databaseConfig.setUser(null); + databaseConfig.setPassword(null); + databaseConfig.setSchema(null); } + setDefaultUserAndPassword(driver); + LoggingProgressMonitor monitor = new LoggingProgressMonitor(log); + driver = migrateDatabaseIfNeeded(monitor, dataSourceProviderRegistry); - String dbUser = databaseConfiguration.getUser(); - String dbPassword = databaseConfiguration.getPassword(); - if (CommonUtils.isEmpty(dbUser) && driver.isEmbedded()) { - File pwdFile = application.getDataDirectory(true).resolve(DEFAULT_DB_PWD_FILE).toFile(); - if (!driver.isAnonymousAccess()) { - // No database credentials specified - dbUser = DEFAULT_DB_USER_NAME; - - // Load or generate random password - if (pwdFile.exists()) { - try (FileReader fr = new FileReader(pwdFile)) { - dbPassword = IOUtils.readToString(fr); - } catch (Exception e) { - log.error(e); - } - } - if (CommonUtils.isEmpty(dbPassword)) { - dbPassword = SecurityUtils.generatePassword(8); - try { - IOUtils.writeFileFromString(pwdFile, dbPassword); - } catch (IOException e) { - log.error(e); - } - } - } - } - String dbURL = GeneralUtils.replaceVariables(databaseConfiguration.getUrl(), SystemVariablesResolver.INSTANCE); - Properties dbProperties = new Properties(); - if (!CommonUtils.isEmpty(dbUser)) { - dbProperties.put(DBConstants.DATA_SOURCE_PROPERTY_USER, dbUser); - if (!CommonUtils.isEmpty(dbPassword)) { - dbProperties.put(DBConstants.DATA_SOURCE_PROPERTY_PASSWORD, dbPassword); - } + // read initial data before connecting to database + // config file must be valid + readInitialDataConfigurationFile(); + + this.dataSource = initConnectionPool(driver.getDefaultDriverLoader().getDriverInstance(monitor), driver.getFullName()); + this.dialect = driver.getScriptDialect().createInstance(); + + try (Connection connection = openConnection()) { + initSchema(monitor, connection); + } catch (Exception e) { + throw new DBException("Error updating management database schema", e); } + log.debug("\tManagement database connection established"); + } - if (H2Migrator.isH2Database(databaseConfiguration)) { - var migrator = new H2Migrator(monitor, dataSourceProviderRegistry, databaseConfiguration, dbURL, dbProperties); + @NotNull + private DBPDriver migrateDatabaseIfNeeded( + @NotNull DBRProgressMonitor monitor, + @NotNull DataSourceProviderRegistry dataSourceProviderRegistry + ) throws DBException { + if (H2Migrator.isH2Database(databaseConfig)) { + var migrator = new H2Migrator( + monitor, + dataSourceProviderRegistry, + databaseConfig, + getProperties() + ); migrator.migrateDatabaseIfNeeded(V1_DB_NAME, V2_DB_NAME); } - // reload the driver and url due to a possible configuration update - driver = dataSourceProviderRegistry.findDriver(databaseConfiguration.getDriver()); - if (driver == null) { - throw new DBException("Driver '" + databaseConfiguration.getDriver() + "' not found"); + return getDatabaseDriver(dataSourceProviderRegistry); + } + + private void setDefaultUserAndPassword(@NotNull DBPDriver driver) { + if (!CommonUtils.isEmpty(databaseConfig.getUser()) || !driver.isEmbedded()) { + return; } - Driver driverInstance = driver.getDriverInstance(monitor); - dbURL = GeneralUtils.replaceVariables(databaseConfiguration.getUrl(), SystemVariablesResolver.INSTANCE); + // No database credentials specified + databaseConfig.setUser(DEFAULT_DB_USER_NAME); - try { - this.cbDataSource = initConnectionPool(driver, dbURL, dbProperties, driverInstance); - } catch (SQLException e) { - throw new DBException("Error initializing connection pool"); + if (driver.isAnonymousAccess()) { + return; } - SQLDialect dialect = driver.getScriptDialect().createInstance(); - - try (Connection connection = cbDataSource.getConnection()) { - DatabaseMetaData metaData = connection.getMetaData(); - log.debug("\tConnected to " + metaData.getDatabaseProductName() + " " + metaData.getDatabaseProductVersion()); - - var schemaName = databaseConfiguration.getSchema(); - if (dialect instanceof SQLDialectSchemaController && CommonUtils.isNotEmpty(schemaName)) { - var dialectSchemaController = (SQLDialectSchemaController) dialect; - var schemaExistQuery = dialectSchemaController.getSchemaExistQuery(schemaName); - boolean schemaExist = JDBCUtils.executeQuery(connection, schemaExistQuery) != null; - if (!schemaExist) { - log.info("Schema " + schemaName + " not exist, create new one"); - String createSchemaQuery = dialectSchemaController.getCreateSchemaQuery( - schemaName - ); - JDBCUtils.executeStatement(connection, createSchemaQuery); - } + File pwdFile = application.getDataDirectory(true).resolve(DEFAULT_DB_PWD_FILE).toFile(); + // Load or generate random password + if (pwdFile.exists()) { + try (FileReader fr = new FileReader(pwdFile)) { + databaseConfig.setPassword(IOUtils.readToString(fr)); + } catch (Exception e) { + log.error(e); } - SQLSchemaManager schemaManager = new SQLSchemaManager( - "CB", - new ClassLoaderScriptSource( - CBDatabase.class.getClassLoader(), - SCHEMA_CREATE_SQL_PATH, - SCHEMA_UPDATE_SQL_PATH - ), - monitor1 -> connection, - new CBSchemaVersionManager(), - dialect, - null, - schemaName, - CURRENT_SCHEMA_VERSION, - 0 - ); - schemaManager.updateSchema(monitor); + } + if (CommonUtils.isEmpty(databaseConfig.getPassword())) { + databaseConfig.setPassword(SecurityUtils.generatePassword(8)); + try { + IOUtils.writeFileFromString(pwdFile, databaseConfig.getPassword()); + } catch (IOException e) { + log.error(e); + } + } + } - validateInstancePersistentState(connection); - } catch (Exception e) { - throw new DBException("Error updating management database schema", e); + @Override + protected void initializeSchema(@NotNull DBRProgressMonitor monitor, @Nullable Connection connection) throws Exception { + if (connection == null) { + throw new DBException("CB database connection is not defined"); } - log.debug("\tManagement database connection established"); + createSchemaIfNotExists(connection); + updateSchema(monitor, connection); + + validateInstancePersistentState(connection); } - protected PoolingDataSource initConnectionPool( - DBPDriver driver, - String dbURL, - Properties dbProperties, - Driver driverInstance - ) throws SQLException, DBException { + // TODO: use a common code for the connection pool init + @NotNull + protected DataSource initConnectionPool(@NotNull Driver driverInstance, @NotNull String driverName) { + final String dbURL = databaseConfig.getResolvedUrl(); // Create connection pool with custom connection factory - log.debug("\tInitiate connection pool with management database (" + driver.getFullName() + "; " + dbURL + ")"); - DriverConnectionFactory conFactory = new DriverConnectionFactory(driverInstance, dbURL, dbProperties); + log.debug("\tInitiate connection pool with management database (" + driverName + "; " + dbURL + ")"); + DriverConnectionFactory conFactory = new DriverConnectionFactory(driverInstance, dbURL, getProperties()); PoolableConnectionFactory pcf = new PoolableConnectionFactory(conFactory, null); - pcf.setValidationQuery(databaseConfiguration.getPool().getValidationQuery()); + pcf.setValidationQuery(databaseConfig.getPool().getValidationQuery()); GenericObjectPoolConfig config = new GenericObjectPoolConfig<>(); - config.setMinIdle(databaseConfiguration.getPool().getMinIdleConnections()); - config.setMaxIdle(databaseConfiguration.getPool().getMaxIdleConnections()); - config.setMaxTotal(databaseConfiguration.getPool().getMaxConnections()); + config.setMinIdle(databaseConfig.getPool().getMinIdleConnections()); + config.setMaxIdle(databaseConfig.getPool().getMaxIdleConnections()); + config.setMaxTotal(databaseConfig.getPool().getMaxConnections()); GenericObjectPool connectionPool = new GenericObjectPool<>(pcf, config); pcf.setPool(connectionPool); return new PoolingDataSource<>(connectionPool); @@ -246,14 +247,13 @@ 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"); } log.info("Configure CB database security"); - CBDatabaseInitialData initialData = getInitialData(); if (initialData != null && !CommonUtils.isEmpty(initialData.getAdminName()) && !CommonUtils.equalObjects(initialData.getAdminName(), adminName) ) { @@ -264,29 +264,29 @@ 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); } } } } - @Nullable - CBDatabaseInitialData getInitialData() throws DBException { - String initialDataPath = databaseConfiguration.getInitialDataConfiguration(); + private void readInitialDataConfigurationFile() throws DBException { + String initialDataPath = databaseConfig.getInitialDataConfiguration(); if (CommonUtils.isEmpty(initialDataPath)) { - return null; + return; } - initialDataPath = WebAppUtils.getRelativePath( - databaseConfiguration.getInitialDataConfiguration(), application.getHomeDirectory()); + initialDataPath = ServletAppUtils.getRelativePath( + databaseConfig.getInitialDataConfiguration(), application.getHomeDirectory()); try (Reader reader = new InputStreamReader(new FileInputStream(initialDataPath), StandardCharsets.UTF_8)) { - Gson gson = new GsonBuilder().setLenient().create(); - return gson.fromJson(reader, CBDatabaseInitialData.class); + Gson gson = new GsonBuilder() + .setStrictness(Strictness.LENIENT) + .create(); + this.initialData = gson.fromJson(reader, CBDatabaseInitialData.class); } catch (Exception e) { throw new DBException("Error loading initial data configuration", e); } @@ -301,7 +301,10 @@ private SMUser createAdminUser( if (adminUser == null) { adminUser = new SMUser(adminName, true, "ADMINISTRATOR"); - adminSecurityController.createUser(adminUser.getUserId(), adminUser.getMetaParameters(), true, adminUser.getAuthRole()); + adminSecurityController.createUser(adminUser.getUserId(), + adminUser.getMetaParameters(), + true, + adminUser.getAuthRole()); } if (!CommonUtils.isEmpty(adminPassword)) { @@ -312,7 +315,8 @@ private SMUser createAdminUser( credentials.put(LocalAuthProviderConstants.CRED_USER, adminUser.getUserId()); credentials.put(LocalAuthProviderConstants.CRED_PASSWORD, clientPassword); - WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(LocalAuthProviderConstants.PROVIDER_ID); + WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance() + .getAuthProvider(LocalAuthProviderConstants.PROVIDER_ID); if (authProvider != null) { adminSecurityController.setUserCredentials(adminUser.getUserId(), authProvider.getId(), credentials); } @@ -333,133 +337,94 @@ private void grantAdminPermissionsToUser(String userId) throws DBException { } public void shutdown() { - log.debug("Shutdown database"); - if (cbDataSource != null) { - try { - cbDataSource.close(); - } catch (SQLException e) { - log.error(e); - } - } + closeConnection(); } - private class CBSchemaVersionManager implements SQLSchemaVersionManager { - @Override - public int getCurrentSchemaVersion(DBRProgressMonitor monitor, Connection connection, String schemaName) throws DBException, SQLException { - // Check and update schema - try { - int version = CommonUtils.toInt(JDBCUtils.executeQuery(connection, - normalizeTableNames("SELECT VERSION FROM {table_prefix}CB_SCHEMA_INFO"))); - return version == 0 ? 1 : version; - } catch (SQLException e) { - try { - Object legacyVersion = CommonUtils.toInt(JDBCUtils.executeQuery(connection, - normalizeTableNames("SELECT SCHEMA_VERSION FROM {table_prefix}CB_SERVER"))); - // Table CB_SERVER exist - this is a legacy schema - return LEGACY_SCHEMA_VERSION; - } catch (SQLException ex) { - // Empty schema. Create it from scratch - return -1; - } + public void fillInitialSchemaData(DBRProgressMonitor monitor, Connection connection) throws DBException, SQLException { + // Set exclusive connection. Otherwise security controller will open a new one and won't see new schema objects. + exclusiveConnection = new DelegatingConnection(connection) { + @Override + public void close() throws SQLException { + // do nothing } - } - - @Override - public int getLatestSchemaVersion() { - return CURRENT_SCHEMA_VERSION; - } + }; - @Override - public void updateCurrentSchemaVersion( - DBRProgressMonitor monitor, - @NotNull Connection connection, - @NotNull String schemaName, - int version - ) throws DBException, SQLException { - var updateCount = JDBCUtils.executeUpdate( - connection, - normalizeTableNames("UPDATE {table_prefix}CB_SCHEMA_INFO SET VERSION=?,UPDATE_TIME=CURRENT_TIMESTAMP"), - version - ); - if (updateCount <= 0) { - JDBCUtils.executeSQL( - connection, - normalizeTableNames("INSERT INTO {table_prefix}CB_SCHEMA_INFO (VERSION,UPDATE_TIME) VALUES(?,CURRENT_TIMESTAMP)"), - version - ); - } - } + try { + // Fill initial data - @Override - //TODO move out - public void fillInitialSchemaData(DBRProgressMonitor monitor, Connection connection) throws DBException, SQLException { - // Set exclusive connection. Otherwise security controller will open a new one and won't see new schema objects. - exclusiveConnection = new DelegatingConnection(connection) { - @Override - public void close() throws SQLException { - // do nothing - } - }; - try { - // Fill initial data + if (initialData == null) { + return; + } - CBDatabaseInitialData initialData = getInitialData(); - if (initialData == null) { - return; + String adminName = initialData.getAdminName(); + String adminPassword = initialData.getAdminPassword(); + List initialTeams = initialData.getTeams(); + String defaultTeam = application.getAppConfiguration().getDefaultUserTeam(); + if (CommonUtils.isNotEmpty(defaultTeam)) { + Set initialTeamNames = initialTeams == null + ? Set.of() + : initialTeams.stream().map(SMTeam::getTeamId).collect(Collectors.toSet()); + if (!initialTeamNames.contains(defaultTeam)) { + throw new DBException("Initial teams configuration doesn't contain default team " + defaultTeam); } - - String adminName = initialData.getAdminName(); - String adminPassword = initialData.getAdminPassword(); - List initialTeams = initialData.getTeams(); - String defaultTeam = application.getAppConfiguration().getDefaultUserTeam(); - if (CommonUtils.isNotEmpty(defaultTeam)) { - Set initialTeamNames = initialTeams == null - ? Set.of() - : initialTeams.stream().map(SMTeam::getTeamId).collect(Collectors.toSet()); - if (!initialTeamNames.contains(defaultTeam)) { - throw new DBException("Initial teams configuration doesn't contain default team " + defaultTeam); - } - } - if (!CommonUtils.isEmpty(initialTeams)) { - // Create teams - for (SMTeam team : initialTeams) { - adminSecurityController.createTeam(team.getTeamId(), team.getName(), team.getDescription(), adminName); - if (adminName != null && !application.isMultiNode()) { - adminSecurityController.setSubjectPermissions( - team.getTeamId(), - new ArrayList<>(team.getPermissions()), - adminName - ); - } + } + if (!CommonUtils.isEmpty(initialTeams)) { + // Create teams + for (SMTeam team : initialTeams) { + adminSecurityController.createTeam( + team.getTeamId(), + team.getName(), + team.getDescription(), + adminName + ); + if (!application.isMultiNode()) { + adminSecurityController.setSubjectPermissions( + team.getTeamId(), + new ArrayList<>(team.getPermissions()), + "initial-data-configuration" + ); } } + } - if (!CommonUtils.isEmpty(adminName)) { - // Create admin user - createAdminUser(adminName, adminPassword); - } - } finally { - exclusiveConnection = null; + if (!CommonUtils.isEmpty(adminName)) { + // Create admin user + createAdminUser(adminName, adminPassword); } + } finally { + exclusiveConnection = null; } } - ////////////////////////////////////////// // Persistence - private void validateInstancePersistentState(Connection connection) throws IOException, SQLException { + protected void validateInstancePersistentState(Connection connection) throws IOException, SQLException, DBException { try (JDBCTransaction txn = new JDBCTransaction(connection)) { checkInstanceRecord(connection); + var defaultTeamId = application.getAppConfiguration().getDefaultUserTeam(); + if (CommonUtils.isNotEmpty(defaultTeamId)) { + var team = adminSecurityController.findTeam(defaultTeamId); + if (team == null) { + log.warn("Default users team not found, create :" + defaultTeamId); + adminSecurityController.createTeam(defaultTeamId, defaultTeamId, null, + ApplicationRegistry.getInstance().getApplication().getName()); + } + } txn.commit(); } } private void checkInstanceRecord(Connection connection) throws SQLException, IOException { - InetAddress localHost = InetAddress.getLocalHost(); - String hostName = localHost.getHostName(); + String hostName; + try { + hostName = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + hostName = "localhost"; + } + byte[] hardwareAddress = RuntimeUtils.getLocalMacAddress(); String macAddress = CommonUtils.toHexString(hardwareAddress); @@ -469,13 +434,14 @@ private void checkInstanceRecord(Connection connection) throws SQLException, IOE String versionName = CommonUtils.truncateString(GeneralUtils.getProductVersion().toString(), 32); boolean hasInstanceRecord = JDBCUtils.queryString(connection, - normalizeTableNames("SELECT HOST_NAME FROM {table_prefix}CB_INSTANCE WHERE INSTANCE_ID=?"), instanceId) != null; + "SELECT HOST_NAME FROM {table_prefix}CB_INSTANCE WHERE INSTANCE_ID=?", + instanceId) != null; if (!hasInstanceRecord) { JDBCUtils.executeSQL( connection, - normalizeTableNames("INSERT INTO {table_prefix}CB_INSTANCE " + + "INSERT INTO {table_prefix}CB_INSTANCE " + "(INSTANCE_ID,MAC_ADDRESS,HOST_NAME,PRODUCT_NAME,PRODUCT_VERSION,UPDATE_TIME)" + - " VALUES(?,?,?,?,?,CURRENT_TIMESTAMP)"), + " VALUES(?,?,?,?,?,CURRENT_TIMESTAMP)", instanceId, macAddress, hostName, @@ -484,9 +450,9 @@ private void checkInstanceRecord(Connection connection) throws SQLException, IOE } else { JDBCUtils.executeSQL( connection, - normalizeTableNames("UPDATE {table_prefix}CB_INSTANCE " + + "UPDATE {table_prefix}CB_INSTANCE " + "SET HOST_NAME=?,PRODUCT_NAME=?,PRODUCT_VERSION=?,UPDATE_TIME=CURRENT_TIMESTAMP " + - "WHERE INSTANCE_ID=?"), + "WHERE INSTANCE_ID=?", hostName, productName, versionName, @@ -494,7 +460,7 @@ private void checkInstanceRecord(Connection connection) throws SQLException, IOE } JDBCUtils.executeSQL( connection, - normalizeTableNames("DELETE FROM {table_prefix}CB_INSTANCE_DETAILS WHERE INSTANCE_ID=?"), + "DELETE FROM {table_prefix}CB_INSTANCE_DETAILS WHERE INSTANCE_ID=?", instanceId); Map instanceDetails = new LinkedHashMap<>(); @@ -505,7 +471,7 @@ private void checkInstanceRecord(Connection connection) throws SQLException, IOE } try (PreparedStatement dbStat = connection.prepareStatement( - normalizeTableNames("INSERT INTO {table_prefix}CB_INSTANCE_DETAILS(INSTANCE_ID,FIELD_NAME,FIELD_VALUE) VALUES(?,?,?)")) + "INSERT INTO {table_prefix}CB_INSTANCE_DETAILS(INSTANCE_ID,FIELD_NAME,FIELD_VALUE) VALUES(?,?,?)") ) { dbStat.setString(1, instanceId); for (Map.Entry ide : instanceDetails.entrySet()) { @@ -517,8 +483,6 @@ private void checkInstanceRecord(Connection connection) throws SQLException, IOE } private String getCurrentInstanceId() throws IOException { - // 12 chars - mac address - String macAddress = CommonUtils.toHexString(RuntimeUtils.getLocalMacAddress()); // 16 chars - workspace ID String workspaceId = DBWorkbench.getPlatform().getWorkspace().getWorkspaceId(); if (workspaceId.length() > 16) { @@ -526,7 +490,7 @@ private String getCurrentInstanceId() throws IOException { } StringBuilder id = new StringBuilder(36); - id.append(macAddress); + id.append("000000000000"); // there was mac address, but it generates dynamically when docker is used id.append(":").append(workspaceId).append(":"); while (id.length() < 36) { id.append("X"); @@ -534,12 +498,21 @@ private String getCurrentInstanceId() throws IOException { return id.toString(); } - /** - * Replaces all predefined prefixes in sql query. - */ - @NotNull - public String normalizeTableNames(@NotNull String sql) { - return CommonUtils.normalizeTableNames(sql, databaseConfiguration.getSchema()); + public static boolean isDefaultH2Configuration(WebDatabaseConfig databaseConfiguration) { + var workspace = ServletAppUtils.getServletApplication().getWorkspaceDirectory(); + var v1Path = workspace.resolve(".data").resolve(V1_DB_NAME); + var v2Path = workspace.resolve(".data").resolve(V2_DB_NAME); + var v1DefaultUrl = "jdbc:h2:" + v1Path; + var v2DefaultUrl = "jdbc:h2:" + v2Path; + return v1DefaultUrl.equals(databaseConfiguration.getUrl()) + || v2DefaultUrl.equals(databaseConfiguration.getUrl()); + } + + protected ServletApplication getApplication() { + return application; + } + + protected SMAdminController getAdminSecurityController() { + return adminSecurityController; } - } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabaseConfig.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabaseConfig.java deleted file mode 100644 index ec56a6d910..0000000000 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabaseConfig.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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.security.db; - -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.registry.storage.InternalDatabaseConfig; - -/** - * Database configuration - */ -public class CBDatabaseConfig implements InternalDatabaseConfig { - private String driver; - private String url; - private String user; - private String password; - private String schema; - - private String initialDataConfiguration; - - private final Pool pool = new Pool(); - - public static class Pool { - private int minIdleConnections = 2; - private int maxIdleConnections = 10; - private int maxConnections = 1000; - private String validationQuery = "SELECT 1"; - - public int getMinIdleConnections() { - return minIdleConnections; - } - - public int getMaxIdleConnections() { - return maxIdleConnections; - } - - public int getMaxConnections() { - return maxConnections; - } - - public String getValidationQuery() { - return validationQuery; - } - } - - @Override - public String getDriver() { - return driver; - } - - public void setDriver(String driver) { - this.driver = driver; - } - - @Override - @NotNull - public String getUrl() { - return url; - } - - public void setUrl(@NotNull String url) { - this.url = url; - } - - @Override - public String getUser() { - return user; - } - - @Override - public String getPassword() { - return password; - } - - public String getInitialDataConfiguration() { - return initialDataConfiguration; - } - - public Pool getPool() { - return pool; - } - - public String getSchema() { - return schema; - } -} diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabaseInitialData.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabaseInitialData.java index 366526767d..7f2879f4f6 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabaseInitialData.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabaseInitialData.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,16 +17,17 @@ package io.cloudbeaver.service.security.db; import org.jkiss.dbeaver.model.security.user.SMTeam; +import org.jkiss.utils.CommonUtils; import java.util.List; class CBDatabaseInitialData { - private String adminName = "cbadmin"; - private String adminPassword = "cbadmin20"; + private String adminName; + private String adminPassword; private List teams; public String getAdminName() { - return adminName; + return CommonUtils.isEmpty(adminName) ? null : adminName.toLowerCase(); } public String getAdminPassword() { diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBSchemaVersionManager.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBSchemaVersionManager.java new file mode 100644 index 0000000000..4ade6fa668 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBSchemaVersionManager.java @@ -0,0 +1,128 @@ +package io.cloudbeaver.service.security.db; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.impl.jdbc.JDBCUtils; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.sql.schema.SQLSchemaVersionManager; +import org.jkiss.utils.CommonUtils; + +import java.sql.Connection; +import java.sql.SQLException; + +public class CBSchemaVersionManager implements SQLSchemaVersionManager { + + private static final Log log = Log.getLog(CBSchemaVersionManager.class); + private static final int LEGACY_SCHEMA_VERSION = 1; + private final int currentSchemaVersion; + private final String schemaId; + + public CBSchemaVersionManager(int currentSchemaVersion, String schemaId) { + this.currentSchemaVersion = currentSchemaVersion; + this.schemaId = schemaId; + } + + @Override + public int getCurrentSchemaVersion( + DBRProgressMonitor monitor, + Connection connection, + String schemaName + ) throws DBException, SQLException { + + Integer version = tryGetVersion( + connection, + "SELECT VERSION FROM {table_prefix}CB_SCHEMA_INFO WHERE MODULE_ID = ?", + getSchemaId() + ); + if (version != null) { + return version; + } + version = tryGetVersion( + connection, + "SELECT VERSION FROM {table_prefix}CB_SCHEMA_INFO" + ); + if (version != null) { + return version; + } + version = tryGetVersion( + connection, + "SELECT SCHEMA_VERSION FROM {table_prefix}CB_SERVER" + ); + if (version != null) { + return LEGACY_SCHEMA_VERSION; + } + // Empty schema. Create it from scratch + return -1; + } + + @Override + public int getLatestSchemaVersion() { + return currentSchemaVersion; + } + + @Override + public void updateCurrentSchemaVersion( + DBRProgressMonitor monitor, + @NotNull Connection connection, + @NotNull String schemaName, + int version + ) throws DBException, SQLException { + var selectCount = CommonUtils.toInt(JDBCUtils.executeQuery( + connection, + "SELECT count(1) FROM {table_prefix}CB_SCHEMA_INFO" + )); + if (selectCount <= 0) { + log.debug("Didn't find any records in " + + CommonUtils.normalizeTableNames("{table_prefix}CB_SCHEMA_INFO", schemaName)); + JDBCUtils.executeSQL( + connection, + "INSERT INTO {table_prefix}CB_SCHEMA_INFO (MODULE_ID, VERSION,UPDATE_TIME) VALUES(?, ?, CURRENT_TIMESTAMP)", + getSchemaId(), + version + ); + return; + } + if (selectCount == 1) { + log.debug("Found only one record in " + + CommonUtils.normalizeTableNames("{table_prefix}CB_SCHEMA_INFO", schemaName)); + JDBCUtils.executeUpdate( + connection, + "UPDATE {table_prefix}CB_SCHEMA_INFO SET VERSION=?,UPDATE_TIME=CURRENT_TIMESTAMP", + version + ); + return; + } + JDBCUtils.executeUpdate( + connection, + "UPDATE {table_prefix}CB_SCHEMA_INFO SET VERSION=?,UPDATE_TIME=CURRENT_TIMESTAMP WHERE MODULE_ID = ?", + version, + getSchemaId() + ); + + } + + protected Integer tryGetVersion(Connection connection, String sql, Object... params) { + return tryGetVersion(connection, sql, 1, params); + } + + protected Integer tryGetVersion(Connection connection, String sql, Integer defaultVersion, Object... params) { + try { + Object result = JDBCUtils.executeQuery(connection, sql, params); + return result == null ? defaultVersion : CommonUtils.toInt(result); + } catch (Exception e) { + try { + connection.rollback(); + } catch (SQLException ex) { + log.error("Can't rollback after unsuccessful try to get version of current schema", ex); + } + return null; + } + } + + + protected String getSchemaId() { + return schemaId; + } +} + diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/AuthAttemptSessionInfo.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/AuthAttemptSessionInfo.java index 24c7803483..7a9d489bb4 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/AuthAttemptSessionInfo.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/AuthAttemptSessionInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -33,17 +33,23 @@ public class AuthAttemptSessionInfo { private final SMSessionType sessionType; @NotNull private final Map sessionParams; + private final boolean mainAuth; + private final boolean serviceAuth; public AuthAttemptSessionInfo( @NotNull String appSessionId, @Nullable String smSessionId, @NotNull SMSessionType sessionType, - @NotNull Map sessionParams + @NotNull Map sessionParams, + boolean mainAuth, + boolean serviceAuth ) { this.appSessionId = appSessionId; this.smSessionId = smSessionId; this.sessionType = sessionType; this.sessionParams = sessionParams; + this.mainAuth = mainAuth; + this.serviceAuth = serviceAuth; } @NotNull @@ -65,4 +71,12 @@ public Map getSessionParams() { public String getSmSessionId() { return smSessionId; } + + public boolean isMainAuth() { + return mainAuth; + } + + public boolean isServiceAuth() { + return serviceAuth; + } } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/CBAuthSubjectRepo.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/CBAuthSubjectRepo.java new file mode 100644 index 0000000000..084f464a58 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/CBAuthSubjectRepo.java @@ -0,0 +1,56 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.service.security.internal; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.security.SMSubjectType; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class CBAuthSubjectRepo { + private static final Log log = Log.getLog(CBAuthSubjectRepo.class); + private static final CBAuthSubjectRepo INSTANCE = new CBAuthSubjectRepo(); + + private CBAuthSubjectRepo() { + } + + public static CBAuthSubjectRepo getInstance() { + return INSTANCE; + } + + public SMSubjectType getSubjectType(@NotNull Connection dbCon, @NotNull String subjectId) { + try { + String sqlBuilder = "SELECT SUBJECT_TYPE FROM {table_prefix}CB_AUTH_SUBJECT U WHERE SUBJECT_ID = ?"; + try (var dbStat = dbCon.prepareStatement(sqlBuilder)) { + dbStat.setString(1, subjectId); + try (ResultSet dbResult = dbStat.executeQuery()) { + if (dbResult.next()) { + return SMSubjectType.fromCode(dbResult.getString(1)); + } + } + } + return null; + } catch (SQLException e) { + log.error("Error getting all subject ids from database", e); + return null; + } + } + +} diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/ClearAuthAttemptInfoJob.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/ClearAuthAttemptInfoJob.java index 4613f8a559..8eb28b5721 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/ClearAuthAttemptInfoJob.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/ClearAuthAttemptInfoJob.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,14 +16,14 @@ */ package io.cloudbeaver.service.security.internal; - import io.cloudbeaver.service.security.CBEmbeddedSecurityController; - import org.eclipse.core.runtime.IStatus; - import org.eclipse.core.runtime.Status; - import org.jkiss.dbeaver.DBException; - import org.jkiss.dbeaver.Log; - import org.jkiss.dbeaver.model.runtime.AbstractJob; - import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; - import org.jkiss.dbeaver.utils.GeneralUtils; +import io.cloudbeaver.service.security.CBEmbeddedSecurityController; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.runtime.AbstractJob; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.utils.CommonUtils; public class ClearAuthAttemptInfoJob extends AbstractJob { @@ -45,7 +45,7 @@ protected IStatus run(DBRProgressMonitor monitor) { securityController.clearOldAuthAttemptInfo(); schedule(CHECK_PERIOD); } catch (DBException e) { - log.error("Error to clear the auth attempt info: " + GeneralUtils.getRootCause(e).getMessage()); + log.error("Error to clear the auth attempt info: " + CommonUtils.getRootCause(e).getMessage()); // Check failed. Re-schedule after 5 seconds schedule(RETRY_PERIOD); } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/SMTokenInfo.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/SMTokenInfo.java index 455b48754d..50c06c18b3 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/SMTokenInfo.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/SMTokenInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -35,19 +35,22 @@ public class SMTokenInfo { @Nullable private final String authRole; + private final boolean serviceToken; public SMTokenInfo( @NotNull String accessToken, @NotNull String refreshToken, @NotNull String sessionId, @NotNull String userId, - @Nullable String authRole + @Nullable String authRole, + boolean serviceToken ) { this.accessToken = accessToken; this.refreshToken = refreshToken; this.sessionId = sessionId; this.userId = userId; this.authRole = authRole; + this.serviceToken = serviceToken; } @NotNull @@ -74,4 +77,8 @@ public String getAccessToken() { public String getAuthRole() { return authRole; } + + public boolean isServiceToken() { + return serviceToken; + } } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/utils/DBConfigurationUtils.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/utils/DBConfigurationUtils.java index a11619245d..d025c18221 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/utils/DBConfigurationUtils.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/utils/DBConfigurationUtils.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,7 +16,7 @@ */ package io.cloudbeaver.service.security.internal.utils; -import io.cloudbeaver.service.security.db.CBDatabaseConfig; +import io.cloudbeaver.model.config.WebDatabaseConfig; import org.jkiss.code.Nullable; import org.jkiss.utils.CommonUtils; @@ -37,7 +37,7 @@ public class DBConfigurationUtils { static final String PARAM_DB_POOL_MAX_CONNECTIONS_CONFIGURATION = "maxConnections"; static final String PARAM_DB_POOL_VALIDATION_QUERY_CONFIGURATION = "validationQuery"; - public static Map databaseConfigToMap(@Nullable CBDatabaseConfig databaseConfiguration) { + public static Map databaseConfigToMap(@Nullable WebDatabaseConfig databaseConfiguration) { Map res = new LinkedHashMap<>(); if (databaseConfiguration == null) { return res; @@ -69,12 +69,12 @@ public static Map databaseConfigToMap(@Nullable CBDatabaseConfig return res; } - public static Map poolDatabaseConfigToMap(@Nullable CBDatabaseConfig databaseConfiguration) { + public static Map poolDatabaseConfigToMap(@Nullable WebDatabaseConfig databaseConfiguration) { Map res = new LinkedHashMap<>(); if (databaseConfiguration == null) { return res; } - CBDatabaseConfig.Pool pool = databaseConfiguration.getPool(); + WebDatabaseConfig.Pool pool = databaseConfiguration.getPool(); if (pool == null) { return res; } else { diff --git a/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..756da7dc26 --- /dev/null +++ b/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF @@ -0,0 +1,13 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Vendor: DBeaver Corp +Bundle-Name: CloudBeaver SLF4j Binding +Bundle-SymbolicName: io.cloudbeaver.slf4j;singleton:=true +Bundle-Version: 1.0.45.qualifier +Bundle-Release-Date: 20250922 +Bundle-RequiredExecutionEnvironment: JavaSE-21 +Bundle-ActivationPolicy: lazy +Fragment-Host: slf4j.api +Require-Bundle: org.jkiss.bundle.logback +Automatic-Module-Name: io.cloudbeaver.slf4j +Provide-Capability: osgi.serviceloader;osgi.serviceloader="org.slf4j.spi.SLF4JServiceProvider" diff --git a/server/bundles/io.cloudbeaver.slf4j/META-INF/services/org.slf4j.spi.SLF4JServiceProvider b/server/bundles/io.cloudbeaver.slf4j/META-INF/services/org.slf4j.spi.SLF4JServiceProvider new file mode 100644 index 0000000000..24a9f55c83 --- /dev/null +++ b/server/bundles/io.cloudbeaver.slf4j/META-INF/services/org.slf4j.spi.SLF4JServiceProvider @@ -0,0 +1 @@ +io.cloudbeaver.slf4j.CloudBeaverLogServiceProvider \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.slf4j/build.properties b/server/bundles/io.cloudbeaver.slf4j/build.properties new file mode 100644 index 0000000000..0511fdeb94 --- /dev/null +++ b/server/bundles/io.cloudbeaver.slf4j/build.properties @@ -0,0 +1,5 @@ +source.. = src/ +output.. = target/classes/ +bin.includes = .,\ + META-INF/,\ + plugin.xml diff --git a/server/bundles/io.cloudbeaver.slf4j/plugin.xml b/server/bundles/io.cloudbeaver.slf4j/plugin.xml new file mode 100644 index 0000000000..cb11a658f5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.slf4j/plugin.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/server/bundles/io.cloudbeaver.slf4j/pom.xml b/server/bundles/io.cloudbeaver.slf4j/pom.xml new file mode 100644 index 0000000000..f454614525 --- /dev/null +++ b/server/bundles/io.cloudbeaver.slf4j/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + io.cloudbeaver + bundles + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.slf4j + 1.0.45-SNAPSHOT + eclipse-plugin + + diff --git a/server/bundles/io.cloudbeaver.slf4j/src/io/cloudbeaver/slf4j/CloudBeaverLogServiceProvider.java b/server/bundles/io.cloudbeaver.slf4j/src/io/cloudbeaver/slf4j/CloudBeaverLogServiceProvider.java new file mode 100644 index 0000000000..6800989ef2 --- /dev/null +++ b/server/bundles/io.cloudbeaver.slf4j/src/io/cloudbeaver/slf4j/CloudBeaverLogServiceProvider.java @@ -0,0 +1,50 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.slf4j; + +import ch.qos.logback.classic.spi.LogbackServiceProvider; +import org.slf4j.helpers.Reporter; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class CloudBeaverLogServiceProvider extends LogbackServiceProvider { + private static final String LOGBACK_CONF_FILE_PROPERTY = "logback.configurationFile"; + private static final String MAIN_LOGBACK_CONFIG = "conf/logback.xml"; + private static final String CUSTOM_LOGBACK_CONFIG = "conf/custom/logback.xml"; + + + public CloudBeaverLogServiceProvider() { + if (System.getProperty(LOGBACK_CONF_FILE_PROPERTY) != null) { + return; + } + + String logbackConfig = null; + if (Files.exists(Path.of(CUSTOM_LOGBACK_CONFIG))) { + logbackConfig = CUSTOM_LOGBACK_CONFIG; + } else if (Files.exists(Path.of(MAIN_LOGBACK_CONFIG))) { + logbackConfig = MAIN_LOGBACK_CONFIG; + } + + if (logbackConfig != null) { + System.setProperty(LOGBACK_CONF_FILE_PROPERTY, Path.of(logbackConfig).toString()); + Reporter.info("Logback configuration is used: " + logbackConfig); + } else { + Reporter.info("No logback configuration found"); + } + } +} diff --git a/server/bundles/pom.xml b/server/bundles/pom.xml index 453987399b..fa644299fe 100644 --- a/server/bundles/pom.xml +++ b/server/bundles/pom.xml @@ -15,6 +15,8 @@ io.cloudbeaver.model io.cloudbeaver.server + io.cloudbeaver.server.ce + io.cloudbeaver.slf4j io.cloudbeaver.service.admin io.cloudbeaver.service.auth @@ -23,7 +25,9 @@ io.cloudbeaver.service.rm io.cloudbeaver.service.rm.nio io.cloudbeaver.service.data.transfer + io.cloudbeaver.service.security + io.cloudbeaver.service.ldap.auth io.cloudbeaver.resources.drivers.base diff --git a/server/drivers/clickhouse/pom.xml b/server/drivers/clickhouse/pom.xml deleted file mode 100644 index e55e276e7f..0000000000 --- a/server/drivers/clickhouse/pom.xml +++ /dev/null @@ -1,32 +0,0 @@ - - 4.0.0 - drivers.clickhouse - 1.0.0 - - io.cloudbeaver - drivers - 1.0.0 - ../ - - - - clickhouse - - - - - ru.yandex.clickhouse - clickhouse-jdbc - shaded - 0.3.1-patch - - - * - * - - - - - - diff --git a/server/drivers/clickhouse_com/pom.xml b/server/drivers/clickhouse_com/pom.xml index c3d83b333d..0f40c3f04a 100644 --- a/server/drivers/clickhouse_com/pom.xml +++ b/server/drivers/clickhouse_com/pom.xml @@ -18,7 +18,19 @@ com.clickhouse clickhouse-jdbc - 0.3.2-patch7 + 0.8.5 + http + + + org.apache.httpcomponents.client5 + httpclient5 + 5.3.1 + + + org.slf4j + slf4j-api + + diff --git a/server/drivers/databend/pom.xml b/server/drivers/databend/pom.xml new file mode 100644 index 0000000000..34f76f982e --- /dev/null +++ b/server/drivers/databend/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + drivers.databend + 1.0.0 + + io.cloudbeaver + drivers + 1.0.0 + ../ + + + + databend + + + + + com.databend + databend-jdbc + 0.3.9 + + + + \ No newline at end of file diff --git a/server/drivers/db2-jt400/pom.xml b/server/drivers/db2-jt400/pom.xml index 5158c8c7f2..05cdc02a0a 100644 --- a/server/drivers/db2-jt400/pom.xml +++ b/server/drivers/db2-jt400/pom.xml @@ -18,7 +18,7 @@ net.sf.jt400 jt400 - 10.5 + 20.0.7 diff --git a/server/drivers/db2/pom.xml b/server/drivers/db2/pom.xml index 6eb5016a46..2e1fdb9b8f 100644 --- a/server/drivers/db2/pom.xml +++ b/server/drivers/db2/pom.xml @@ -18,7 +18,7 @@ com.ibm.db2 jcc - 11.5.7.0 + 11.5.9.0 diff --git a/server/drivers/derby/pom.xml b/server/drivers/derby/pom.xml deleted file mode 100644 index e54b0163f3..0000000000 --- a/server/drivers/derby/pom.xml +++ /dev/null @@ -1,40 +0,0 @@ - - 4.0.0 - drivers.derby - 1.0.0 - - io.cloudbeaver - drivers - 1.0.0 - ../ - - - - derby - - - - - org.apache.derby - derby - 10.15.1.3 - - - org.apache.derby - derbytools - 10.15.1.3 - - - org.apache.derby - derbyshared - 10.15.1.3 - - - org.apache.derby - derbyclient - 10.15.1.3 - - - - diff --git a/server/drivers/duckdb/pom.xml b/server/drivers/duckdb/pom.xml new file mode 100644 index 0000000000..4ff2c96615 --- /dev/null +++ b/server/drivers/duckdb/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + drivers.duckdb + 1.0.0 + + io.cloudbeaver + drivers + 1.0.0 + ../ + + + + duckdb + + + + + org.duckdb + duckdb_jdbc + 1.3.2.0 + + + + diff --git a/server/drivers/h2/lib/h2-legacy.jar b/server/drivers/h2/lib/h2-legacy.jar index 93c243413b..4230477ec1 100644 Binary files a/server/drivers/h2/lib/h2-legacy.jar and b/server/drivers/h2/lib/h2-legacy.jar differ diff --git a/server/drivers/h2_v2/lib/h2-legacy-2.jar b/server/drivers/h2_v2/lib/h2-legacy-2.jar new file mode 100644 index 0000000000..9a3fb84e1c Binary files /dev/null and b/server/drivers/h2_v2/lib/h2-legacy-2.jar differ diff --git a/server/drivers/h2_v2/pom.xml b/server/drivers/h2_v2/pom.xml index 7afb315480..002ba7eccb 100644 --- a/server/drivers/h2_v2/pom.xml +++ b/server/drivers/h2_v2/pom.xml @@ -14,12 +14,35 @@ h2_v2 - - - com.h2database - h2 - 2.1.214 - - + + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + + copy-legacy-jar + validate + + ../../../deploy/drivers/h2_v2 + true + + + lib + + *.jar + + + + + + copy-resources + + + + + + diff --git a/server/drivers/hive2/pom.xml b/server/drivers/hive2/pom.xml new file mode 100644 index 0000000000..e4dcf6c9a3 --- /dev/null +++ b/server/drivers/hive2/pom.xml @@ -0,0 +1,36 @@ + + 4.0.0 + drivers.hive2 + 1.0.0 + + io.cloudbeaver + drivers + 1.0.0 + ../ + + + + hive2 + + + + + org.apache.hive + hive-jdbc + 2.3.9 + + + jdk.tools + jdk.tools + + + org.slf4j + slf4j-log4j12 + + + + + + + diff --git a/server/drivers/hive4/pom.xml b/server/drivers/hive4/pom.xml new file mode 100644 index 0000000000..f6ca774549 --- /dev/null +++ b/server/drivers/hive4/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + drivers.hive + 1.0.0 + + io.cloudbeaver + drivers + 1.0.0 + ../ + + + + hive + + + + + org.apache.hive + hive-jdbc + 4.0.1 + standalone + + + jdk.tools + jdk.tools + + + + + + + diff --git a/server/drivers/jaybird/pom.xml b/server/drivers/jaybird/pom.xml index 4c8565ab1f..6e817e6f23 100644 --- a/server/drivers/jaybird/pom.xml +++ b/server/drivers/jaybird/pom.xml @@ -18,7 +18,7 @@ org.firebirdsql.jdbc jaybird - 5.0.2.java11 + 5.0.4.java11 diff --git a/server/drivers/kyuubi/pom.xml b/server/drivers/kyuubi/pom.xml new file mode 100644 index 0000000000..d59c25552b --- /dev/null +++ b/server/drivers/kyuubi/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + drivers.kyuubi + 1.0.0 + + io.cloudbeaver + drivers + 1.0.0 + ../ + + + + kyuubi + + + + + org.apache.kyuubi + kyuubi-hive-jdbc-shaded + 1.9.0 + + + + diff --git a/server/drivers/mariadb/pom.xml b/server/drivers/mariadb/pom.xml index ade46c7995..419c40c78d 100644 --- a/server/drivers/mariadb/pom.xml +++ b/server/drivers/mariadb/pom.xml @@ -18,7 +18,7 @@ org.mariadb.jdbc mariadb-java-client - 2.4.3 + 3.5.1 diff --git a/server/drivers/mysql/pom.xml b/server/drivers/mysql/pom.xml index b1bc8ccc3d..76959e3ab1 100644 --- a/server/drivers/mysql/pom.xml +++ b/server/drivers/mysql/pom.xml @@ -16,9 +16,13 @@ - mysql - mysql-connector-java - 8.0.28 + com.mysql + mysql-connector-j + 8.2.0 + + + com.google.protobuf + protobuf-java diff --git a/server/drivers/oracle/pom.xml b/server/drivers/oracle/pom.xml index 13c7cf470a..f5cd7a18e3 100644 --- a/server/drivers/oracle/pom.xml +++ b/server/drivers/oracle/pom.xml @@ -12,28 +12,29 @@ oracle + 23.2.0.0 com.oracle.database.jdbc - ojdbc8 - 12.2.0.1 + ojdbc11 + ${oracle.version} com.oracle.database.nls orai18n - 12.2.0.1 + ${oracle.version} com.oracle.database.xml - xdb6 - 12.2.0.1 + xdb + ${oracle.version} com.oracle.database.xml xmlparserv2 - 12.2.0.1 + ${oracle.version} diff --git a/server/drivers/pom.xml b/server/drivers/pom.xml index 4e8d6e6803..820f775925 100644 --- a/server/drivers/pom.xml +++ b/server/drivers/pom.xml @@ -1,7 +1,12 @@ 4.0.0 - io.cloudbeaver + + io.cloudbeaver + cloudbeaver + 1.0.0-SNAPSHOT + ../ + drivers 1.0.0 pom @@ -11,14 +16,17 @@ - clickhouse clickhouse_com + databend db2 db2-jt400 - derby + duckdb h2 h2_v2 + hive2 + hive4 jaybird + kyuubi mysql mariadb oracle diff --git a/server/drivers/postgresql/pom.xml b/server/drivers/postgresql/pom.xml index 79f7c1c225..a0e12cbf1c 100644 --- a/server/drivers/postgresql/pom.xml +++ b/server/drivers/postgresql/pom.xml @@ -18,7 +18,7 @@ org.postgresql postgresql - 42.5.0 + 42.7.2 net.postgis diff --git a/server/drivers/sqlite/pom.xml b/server/drivers/sqlite/pom.xml index afab9e8310..841969b40d 100644 --- a/server/drivers/sqlite/pom.xml +++ b/server/drivers/sqlite/pom.xml @@ -18,7 +18,7 @@ org.xerial sqlite-jdbc - 3.42.0.0 + 3.48.0.0 diff --git a/server/drivers/sqlserver/pom.xml b/server/drivers/sqlserver/pom.xml index 2ae93c9fe7..71138b49aa 100644 --- a/server/drivers/sqlserver/pom.xml +++ b/server/drivers/sqlserver/pom.xml @@ -18,7 +18,7 @@ com.microsoft.sqlserver mssql-jdbc - 8.2.0.jre8 + 12.8.0.jre11 - @@ -31,7 +29,9 @@ + + @@ -39,12 +39,15 @@ + + + diff --git a/server/features/io.cloudbeaver.server.feature/pom.xml b/server/features/io.cloudbeaver.server.feature/pom.xml index 6be9d6757c..c0a43df211 100644 --- a/server/features/io.cloudbeaver.server.feature/pom.xml +++ b/server/features/io.cloudbeaver.server.feature/pom.xml @@ -10,6 +10,6 @@ ../ io.cloudbeaver.server.feature - 23.2.2-SNAPSHOT + 25.2.1-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.test.feature/build.properties b/server/features/io.cloudbeaver.test.feature/build.properties new file mode 100644 index 0000000000..b3a611b5c9 --- /dev/null +++ b/server/features/io.cloudbeaver.test.feature/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/server/features/io.cloudbeaver.test.feature/feature.properties b/server/features/io.cloudbeaver.test.feature/feature.properties new file mode 100644 index 0000000000..bac6d075f2 --- /dev/null +++ b/server/features/io.cloudbeaver.test.feature/feature.properties @@ -0,0 +1,4 @@ +featureName=Cloudbeaver Test feature +providerName=DBeaver Corp +description=Cloudbeaver Test feature +copyrightcopyright=\u00A9 2010-2024, DBeaver Corp diff --git a/server/features/io.cloudbeaver.test.feature/feature.xml b/server/features/io.cloudbeaver.test.feature/feature.xml new file mode 100644 index 0000000000..8f8ce6193b --- /dev/null +++ b/server/features/io.cloudbeaver.test.feature/feature.xml @@ -0,0 +1,20 @@ + + + + + %description + + + + %copyright + + + + + + diff --git a/server/features/io.cloudbeaver.test.feature/pom.xml b/server/features/io.cloudbeaver.test.feature/pom.xml new file mode 100644 index 0000000000..62b1f676aa --- /dev/null +++ b/server/features/io.cloudbeaver.test.feature/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + io.cloudbeaver + features + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.test.feature + 1.0.0-SNAPSHOT + eclipse-feature + diff --git a/server/features/io.cloudbeaver.ws.feature/feature.properties b/server/features/io.cloudbeaver.ws.feature/feature.properties index 7e1d44bf28..429328d62f 100644 --- a/server/features/io.cloudbeaver.ws.feature/feature.properties +++ b/server/features/io.cloudbeaver.ws.feature/feature.properties @@ -1,4 +1,4 @@ featureName=Cloudbeaver Web Services feature providerName=DBeaver Corp description=Cloudbeaver Web Services feature -copyright=DBeaver Corp, 2022 +copyrightcopyright=\u00A9 2010-2024, DBeaver Corp diff --git a/server/features/io.cloudbeaver.ws.feature/feature.xml b/server/features/io.cloudbeaver.ws.feature/feature.xml index 010ede7563..47bcc54f5f 100644 --- a/server/features/io.cloudbeaver.ws.feature/feature.xml +++ b/server/features/io.cloudbeaver.ws.feature/feature.xml @@ -2,7 +2,7 @@ @@ -15,10 +15,9 @@ - - + @@ -35,13 +34,9 @@ - - - - - + diff --git a/server/features/io.cloudbeaver.ws.feature/pom.xml b/server/features/io.cloudbeaver.ws.feature/pom.xml index 659b43d69b..f762534ed7 100644 --- a/server/features/io.cloudbeaver.ws.feature/pom.xml +++ b/server/features/io.cloudbeaver.ws.feature/pom.xml @@ -10,6 +10,6 @@ ../ io.cloudbeaver.ws.feature - 1.0.36-SNAPSHOT + 1.0.83-SNAPSHOT eclipse-feature diff --git a/server/pom.xml b/server/pom.xml index d9c82ac0c7..8afd2a8626 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -19,19 +19,28 @@ CloudBeaver CE - 23.2.2 + 25.2.1 bundles features - drivers - test - - - product + + + + full-build + !plain-api-server + + drivers + + product + test + + + + diff --git a/server/product/aggregate/build-full.cmd b/server/product/aggregate/build-full.cmd index 6aacb1a7a6..c660606cb6 100644 --- a/server/product/aggregate/build-full.cmd +++ b/server/product/aggregate/build-full.cmd @@ -1,6 +1,6 @@ @echo off set MAVEN_OPTS=-Xmx2048m -call mvn clean install -Dheadless-platform +call mvn clean install -Dheadless-platform -T 1C pause diff --git a/server/product/aggregate/pom.xml b/server/product/aggregate/pom.xml index 2daf10f079..c6aba5e6d0 100644 --- a/server/product/aggregate/pom.xml +++ b/server/product/aggregate/pom.xml @@ -11,6 +11,8 @@ 1.0.0-SNAPSHOT + + ../../../../dbeaver-common ../../../../dbeaver diff --git a/server/product/web-server-test/CloudbeaverServerUnitTest.product b/server/product/web-server-test/CloudbeaverServerUnitTest.product new file mode 100644 index 0000000000..db340de85a --- /dev/null +++ b/server/product/web-server-test/CloudbeaverServerUnitTest.product @@ -0,0 +1,75 @@ + + + + + + + + + -web-config conf/cloudbeaver.conf -registryMultiLanguage + + + -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 + --add-opens=java.sql/java.sql=ALL-UNNAMED + + + + + + https://cloudbeaver.io/about/ + + CloudBeaver - Cloud Database Manager + Copyright (C) 2019-2022 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. + + + + + + + + + + + + + + + + + + diff --git a/server/product/web-server-test/pom.xml b/server/product/web-server-test/pom.xml new file mode 100644 index 0000000000..2a87413b67 --- /dev/null +++ b/server/product/web-server-test/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + io.cloudbeaver + cloudbeaver + 1.0.0-SNAPSHOT + ../../ + + 24.3.2-SNAPSHOT + web-server-test + eclipse-repository + Cloudbeaver Server Product + + + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + + all + all + all + + + + + + + org.eclipse.tycho + tycho-p2-director-plugin + ${tycho-version} + + + + io.cloudbeaver.product + cloudbeaver-server-${dbeaver-version} + + + + + + materialize-products + + materialize-products + + + + archive-products + + + + + + + + + org.eclipse.tycho + tycho-p2-repository-plugin + ${tycho-version} + + false + true + + + + + + diff --git a/server/product/web-server-test/product-branding.properties b/server/product/web-server-test/product-branding.properties new file mode 100644 index 0000000000..74c54fa047 --- /dev/null +++ b/server/product/web-server-test/product-branding.properties @@ -0,0 +1,14 @@ +packageName=cloudbeaver +appName=CloudBeaver +productEclipseId=io.cloudbeaver.product +productModifier=ce +productName=CloudBeaver +productBrandText=Cloud Database Manager +productDescription=Cloud Database Manager +productTrademark=DBeaver is a trademark of DBeaver Corp +productCopyright=DBeaver Corp +productEmail=support@dbeaver.com +productCompany=DBeaver Corp +productWebSite=https://dbeaver.com/ +productBundleId=io.cloudbeaver.product +productRepository=dbeaver/cloudbeaver diff --git a/server/product/web-server/CloudbeaverServer.product b/server/product/web-server/CloudbeaverServer.product index f1e6e707d8..9c7b18514c 100644 --- a/server/product/web-server/CloudbeaverServer.product +++ b/server/product/web-server/CloudbeaverServer.product @@ -2,12 +2,42 @@ - + + -web-config conf/cloudbeaver.conf -registryMultiLanguage + + + -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 + --add-opens=java.sql/java.sql=ALL-UNNAMED + + + + https://cloudbeaver.io/about/ diff --git a/server/product/web-server/pom.xml b/server/product/web-server/pom.xml index 238d1906b8..09893b6d38 100644 --- a/server/product/web-server/pom.xml +++ b/server/product/web-server/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../ - 23.2.2-SNAPSHOT + 25.2.1-SNAPSHOT web-server eclipse-repository Cloudbeaver Server Product diff --git a/server/product/web-server/run.cmd b/server/product/web-server/run.cmd deleted file mode 100644 index 289ed27939..0000000000 --- a/server/product/web-server/run.cmd +++ /dev/null @@ -1 +0,0 @@ -java -jar plugins/org.eclipse.equinox.launcher_1.5.600.v20191014-2022.jar -product io.cloudbeaver.server.product -data C:/data/cloudbeaver/workspace diff --git a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF index 55384ba6ab..031a387de7 100644 --- a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF +++ b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF @@ -5,25 +5,30 @@ Bundle-SymbolicName: io.cloudbeaver.test.platform Bundle-Version: 1.0.0.qualifier Bundle-Release-Date: 20220606 Bundle-Vendor: DBeaver Corp -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy Require-Bundle: org.eclipse.core.runtime, - org.eclipse.core.resources, org.junit, org.mockito.mockito-core, org.apache.felix.scr, org.jkiss.dbeaver.model, + org.jkiss.dbeaver.osgi.test.runner;visibility:=reexport, org.jkiss.dbeaver.model.sql, org.jkiss.dbeaver.registry, org.jkiss.dbeaver.ext.generic, org.jkiss.dbeaver.ext.h2, org.jkiss.dbeaver.ext.postgresql, - org.jkiss.bundle.jetty.websocket, + org.jkiss.bundle.jetty.server, io.cloudbeaver.model, io.cloudbeaver.server, + io.cloudbeaver.server.ce, io.cloudbeaver.resources.drivers.base, io.cloudbeaver.product.ce, io.cloudbeaver.service.auth, io.cloudbeaver.service.rm, - io.cloudbeaver.service.rm.nio + io.cloudbeaver.service.rm.nio, + org.jkiss.dbeaver.ext.mysql, + org.jkiss.dbeaver.ext.postgresql, + org.jkiss.dbeaver.ext.oracle, + org.jkiss.dbeaver.ext.mssql Export-Package: io.cloudbeaver.test.platform diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/CloudbeaverMockTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/CloudbeaverMockTest.java new file mode 100644 index 0000000000..16cdc181f4 --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/CloudbeaverMockTest.java @@ -0,0 +1,29 @@ +package io.cloudbeaver; + +import io.cloudbeaver.utils.WebTestUtils; +import org.jkiss.junit.osgi.OSGITestRunner; +import org.jkiss.junit.osgi.annotation.RunWithApplication; +import org.jkiss.junit.osgi.annotation.RunWithProduct; +import org.jkiss.junit.osgi.annotation.RunnerProxy; +import org.jkiss.junit.osgi.behaviors.IAsyncApplication; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.CookieManager; +import java.net.http.HttpClient; + +@RunWithProduct("CloudbeaverServerUnitTest.product") +@RunnerProxy(MockitoJUnitRunner.class) +@RunWith(OSGITestRunner.class) +@RunWithApplication(bundleName = "io.cloudbeaver.server.ce", registryName = "io.cloudbeaver.product.ce.application", args = {"-web-config", "workspace/conf/cloudbeaver.conf"}) +public abstract class CloudbeaverMockTest implements IAsyncApplication { + private static final String GQL_API_URL = "http://localhost:18978/api/gql"; + private static final String SERVER_STATUS_URL = "http://localhost:18978/status"; + private final HttpClient httpClient = HttpClient.newBuilder() + .cookieHandler(new CookieManager()) + .version(HttpClient.Version.HTTP_2) + .build();; + public boolean verifyLaunched() { + return WebTestUtils.getServerStatus(httpClient, SERVER_STATUS_URL); + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/app/CEAppStarter.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/app/CEAppStarter.java new file mode 100644 index 0000000000..22cb8110a9 --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/app/CEAppStarter.java @@ -0,0 +1,109 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.app; + +import io.cloudbeaver.auth.provider.local.LocalAuthProvider; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBApplicationCE; +import io.cloudbeaver.test.WebGQLClient; +import io.cloudbeaver.utils.WebTestUtils; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.utils.SecurityUtils; +import org.junit.AfterClass; + +import java.net.CookieManager; +import java.net.http.HttpClient; +import java.util.Map; + +public class CEAppStarter { + public static final String SERVER_URL = "http://localhost:18978"; + private static final String GQL_API_URL = SERVER_URL + "/api/gql"; + private static final String SERVER_STATUS_URL = SERVER_URL + "/status"; + private static final Map TEST_CREDENTIALS = Map.of( + LocalAuthProvider.CRED_USER, "test", + LocalAuthProvider.CRED_PASSWORD, SecurityUtils.makeDigest("test") + ); + + private static CBApplication testApp; + + public static void startServerIfNotStarted() throws Exception { + System.out.println("Start CBApplication"); + if (DBWorkbench.isPlatformStarted() && DBWorkbench.getPlatform().getApplication() instanceof CBApplication) { + testApp = (CBApplication) DBWorkbench.getPlatform().getApplication(); + return; + } + testApp = new CBApplicationCE(); + Thread thread = new Thread(() -> testApp.start(null)); + thread.start(); + HttpClient httpClient = HttpClient.newBuilder() + .cookieHandler(new CookieManager()) + .version(HttpClient.Version.HTTP_2) + .build(); + long startTime = System.currentTimeMillis(); + long endTime = 0; + boolean setUpIsDone = false; + while (!setUpIsDone && endTime < 300000) { + setUpIsDone = WebTestUtils.getServerStatus(httpClient, SERVER_STATUS_URL); + endTime = System.currentTimeMillis() - startTime; + } + if (!setUpIsDone) { + throw new Exception("Server is not running"); + } + } + + @AfterClass + public static void shutdownServer() { + testApp.stop(); + } + + public static CBApplication getTestApp() { + return testApp; + } + + public static WebGQLClient createClient() { + HttpClient httpClient = HttpClient.newBuilder() + .cookieHandler(new CookieManager()) + .version(HttpClient.Version.HTTP_2) + .build(); + return createClient(httpClient); + } + + public static WebGQLClient createClient(@NotNull HttpClient httpClient) { + return new WebGQLClient(httpClient, GQL_API_URL); + } + + public static Map authenticateTestUser(@NotNull WebGQLClient client) throws Exception { + return authenticateTestUser(client, TEST_CREDENTIALS); + } + + public static Map authenticateTestUser( + @NotNull WebGQLClient client, + @NotNull Map credentials + ) throws Exception { + return client.sendQuery( + WebGQLClient.GQL_AUTHENTICATE, + Map.of( + "provider", LocalAuthProvider.PROVIDER_ID, + "credentials", credentials + ) + ); + } + + private CEAppStarter() { + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/RMNIOTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/RMNIOTest.java index 80ca252be3..4348d45401 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/RMNIOTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/RMNIOTest.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -16,13 +16,15 @@ */ package io.cloudbeaver.model.rm; +import io.cloudbeaver.CloudbeaverMockTest; +import io.cloudbeaver.app.CEAppStarter; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.CBConstants; import io.cloudbeaver.service.rm.nio.RMNIOFileSystem; import io.cloudbeaver.service.rm.nio.RMNIOFileSystemProvider; import io.cloudbeaver.service.rm.nio.RMPath; +import io.cloudbeaver.test.WebGQLClient; import io.cloudbeaver.test.platform.CEServerTestSuite; -import io.cloudbeaver.utils.WebTestUtils; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.auth.SMAuthStatus; import org.jkiss.dbeaver.model.data.json.JSONUtils; @@ -43,8 +45,9 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; -public class RMNIOTest { +public class RMNIOTest extends CloudbeaverMockTest { private static WebSession webSession; private static RMProject testProject; @@ -53,12 +56,13 @@ public class RMNIOTest { @BeforeClass public static void init() throws Exception { var cookieManager = new CookieManager(); - var client = HttpClient.newBuilder() + CEAppStarter.startServerIfNotStarted(); + var httpClient = HttpClient.newBuilder() .cookieHandler(cookieManager) .version(HttpClient.Version.HTTP_2) .build(); - Map authInfo = WebTestUtils.authenticateUser( - client, CEServerTestSuite.getScriptsPath(), CEServerTestSuite.GQL_API_URL); + WebGQLClient client = CEAppStarter.createClient(httpClient); + Map authInfo = CEAppStarter.authenticateTestUser(client); Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); String sessionId = cookieManager.getCookieStore().getCookies() @@ -67,7 +71,7 @@ public static void init() throws Exception { .findFirst() .get() .getValue(); - webSession = (WebSession) CEServerTestSuite.getTestApp().getSessionManager().getSession(sessionId); + webSession = (WebSession) CEAppStarter.getTestApp().getSessionManager().getSession(sessionId); Assert.assertNotNull(webSession); var projectName = "NIO_Test" + SecurityUtils.generateUniqueId(); testProject = webSession.getRmController().createProject(projectName, null); @@ -135,12 +139,14 @@ public void testListFiles() throws DBException, IOException { String file2 = "script" + SecurityUtils.generateUniqueId() + ".sql"; rm.createResource(testProject.getId(), file1, true); rm.createResource(testProject.getId(), file2, false); - Set filesFromNio = - Files.list(rootPath) - .map(path -> ((RMPath) path).getResourcePath()) - .collect(Collectors.toSet()); - Assert.assertTrue(filesFromNio.contains(file1)); - Assert.assertTrue(filesFromNio.contains(file2)); + try (Stream list = Files.list(rootPath)) { + Set filesFromNio = + list + .map(path -> ((RMPath) path).getResourcePath()) + .collect(Collectors.toSet()); + Assert.assertTrue(filesFromNio.contains(file1)); + Assert.assertTrue(filesFromNio.contains(file2)); + } } @Test diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/lock/RMLockTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/lock/RMLockTest.java index 1c55b82599..a92f0a701b 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/lock/RMLockTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/lock/RMLockTest.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,10 +16,10 @@ */ package io.cloudbeaver.model.rm.lock; -import io.cloudbeaver.test.platform.CEServerTestSuite; +import io.cloudbeaver.CloudbeaverMockTest; +import io.cloudbeaver.app.CEAppStarter; import org.jkiss.dbeaver.Log; -import org.junit.Assert; -import org.junit.Test; +import org.junit.*; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -31,15 +31,25 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -public class RMLockTest { +public class RMLockTest extends CloudbeaverMockTest { private static final Log log = Log.getLog(RMLockTest.class); private final String project1 = "s_fakeProject1"; private final String project2 = "s_fakeProject2"; - private final ExecutorService executor = Executors.newFixedThreadPool(2); + private static final ExecutorService executor = Executors.newFixedThreadPool(2); + + @AfterClass + public static void shutdown() { + executor.shutdown(); + } + + @BeforeClass + public static void startServer() throws Exception { + CEAppStarter.startServerIfNotStarted(); + } @Test public void testProjectAccessUsingSeveralControllers() throws Throwable { - var lockController1 = new TestLockController(CEServerTestSuite.getTestApp(), 1); + var lockController1 = new TestLockController(CEAppStarter.getTestApp(), 1); CountDownLatch thread1CDL = new CountDownLatch(1); CountDownLatch thread2CDL = new CountDownLatch(1); @@ -49,7 +59,7 @@ public void testProjectAccessUsingSeveralControllers() throws Throwable { AtomicReference exceptionReference = new AtomicReference<>(); Runnable runnable1 = () -> { - try (var lock = lockController1.lockProject(project1, "testThatProjectLocked1")) { + try (var lock = lockController1.lock(project1, "testThatProjectLocked1")) { isLockedByThread1.set(true); thread2CDL.countDown(); thread1CDL.await(1, TimeUnit.MINUTES); @@ -63,7 +73,7 @@ public void testProjectAccessUsingSeveralControllers() throws Throwable { }; int atLeastWaitCalls = 1; - var lockController2 = Mockito.spy(new TestLockController(CEServerTestSuite.getTestApp(), 1000)); + var lockController2 = Mockito.spy(new TestLockController(CEAppStarter.getTestApp(), 1000)); Mockito.doAnswer(new Answer() { private int count = 0; @@ -76,15 +86,15 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable { } return invocationOnMock.callRealMethod(); } - }).when(lockController2).awaitingUnlock(Mockito.any(), Mockito.any()); + }).when(lockController2).awaitingUnlock(Mockito.any()); Runnable runnable2 = () -> { try { thread2CDL.await(1, TimeUnit.MINUTES); Assert.assertTrue("Project not locket by thread 1", isLockedByThread1.get()); - Assert.assertTrue("Project not locked", lockController2.isProjectLocked(project1)); - try (var lock = lockController2.lockProject(project1, "testThatProjectLocked2")) { + Assert.assertTrue("Project not locked", lockController2.isFileLocked(project1)); + try (var lock = lockController2.lock(project1, "testThatProjectLocked2")) { //that we were really waiting for the file and the lock was not removed earlier - Mockito.verify(lockController2, Mockito.atLeast(atLeastWaitCalls)).awaitingUnlock(Mockito.any(), Mockito.any()); + Mockito.verify(lockController2, Mockito.atLeast(atLeastWaitCalls)).awaitingUnlock(Mockito.any()); } } catch (Throwable e) { log.error(e); @@ -100,12 +110,12 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable { if (exceptionReference.get() != null) { throw exceptionReference.get(); } - Assert.assertFalse(lockController2.isProjectLocked(project1)); + Assert.assertFalse(lockController2.isFileLocked(project1)); } @Test public void testAccessToDifferentProjects() throws Throwable { - var lockController1 = new TestLockController(CEServerTestSuite.getTestApp(), 1); + var lockController1 = new TestLockController(CEAppStarter.getTestApp(), 1); CountDownLatch thread1CDL = new CountDownLatch(1); CountDownLatch thread2CDL = new CountDownLatch(1); @@ -116,7 +126,7 @@ public void testAccessToDifferentProjects() throws Throwable { AtomicBoolean isLockedByThread2 = new AtomicBoolean(false); AtomicReference exceptionReference = new AtomicReference<>(); Runnable runnable1 = () -> { - try (var lock = lockController1.lockProject(project1, "testAccessToDifferentProjects1")) { + try (var lock = lockController1.lock(project1, "testAccessToDifferentProjects1")) { isLockedByThread1.set(true); thread2InitCDL.countDown(); thread1CDL.await(1, TimeUnit.MINUTES); @@ -131,10 +141,10 @@ public void testAccessToDifferentProjects() throws Throwable { } }; - var lockController2 = new TestLockController(CEServerTestSuite.getTestApp(), 1); + var lockController2 = new TestLockController(CEAppStarter.getTestApp(), 1); Runnable runnable2 = () -> { try { - try (var lock = lockController2.lockProject(project2, "testAccessToDifferentProjects2")) { + try (var lock = lockController2.lock(project2, "testAccessToDifferentProjects2")) { thread2InitCDL.await(); Assert.assertTrue("Project1 not locket by thread1", isLockedByThread1.get()); isLockedByThread2.set(true); @@ -157,13 +167,13 @@ public void testAccessToDifferentProjects() throws Throwable { throw exceptionReference.get(); } - Assert.assertFalse(lockController2.isProjectLocked(project1)); - Assert.assertFalse(lockController2.isProjectLocked(project2)); + Assert.assertFalse(lockController2.isFileLocked(project1)); + Assert.assertFalse(lockController2.isFileLocked(project2)); } @Test public void testForceUnlock() throws Throwable { - var lockController1 = new TestLockController(CEServerTestSuite.getTestApp(), 1); + var lockController1 = new TestLockController(CEAppStarter.getTestApp(), 1); CountDownLatch thread1CDL = new CountDownLatch(1); CountDownLatch globalCountDown = new CountDownLatch(2); @@ -171,7 +181,7 @@ public void testForceUnlock() throws Throwable { AtomicBoolean isLockedByThread1 = new AtomicBoolean(false); AtomicReference exceptionReference = new AtomicReference<>(); Runnable runnable1 = () -> { - try (var lock = lockController1.lockProject(project1, "testForceUnlock1")) { + try (var lock = lockController1.lock(project1, "testForceUnlock1")) { isLockedByThread1.set(true); thread1CDL.await(1, TimeUnit.MINUTES); } catch (Throwable e) { @@ -183,10 +193,10 @@ public void testForceUnlock() throws Throwable { } }; - var lockController2 = Mockito.spy(new TestLockController(CEServerTestSuite.getTestApp(), 100)); + var lockController2 = Mockito.spy(new TestLockController(CEAppStarter.getTestApp(), 100)); Runnable runnable2 = () -> { try { - try (var lock = lockController2.lockProject(project1, "testForceUnlock2")) { + try (var lock = lockController2.lock(project1, "testForceUnlock2")) { Assert.assertTrue("Project1 not locket by thread1", isLockedByThread1.get()); Mockito.verify(lockController2, Mockito.atLeast(5)).isLocked(Mockito.any()); thread1CDL.countDown(); @@ -205,6 +215,6 @@ public void testForceUnlock() throws Throwable { if (exceptionReference.get() != null) { throw exceptionReference.get(); } - Assert.assertFalse(lockController2.isProjectLocked(project1)); + Assert.assertFalse(lockController2.isFileLocked(project1)); } } diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/lock/TestLockController.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/lock/TestLockController.java index 74dcc31098..1d55b5040c 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/lock/TestLockController.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/lock/TestLockController.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -16,29 +16,32 @@ */ package io.cloudbeaver.model.rm.lock; -import io.cloudbeaver.model.app.WebApplication; +import io.cloudbeaver.model.app.ServletApplication; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.fs.lock.FileLockController; +import org.jkiss.dbeaver.utils.GeneralUtils; import java.nio.file.Path; -public class TestLockController extends RMFileLockController { - public TestLockController(WebApplication application) throws DBException { - super(application); +public class TestLockController extends FileLockController { + public TestLockController(ServletApplication application) throws DBException { + super(application.getApplicationInstanceId()); } - public TestLockController(WebApplication application, int maxTimeout) throws DBException { - super(application, maxTimeout); + public TestLockController(ServletApplication application, int maxTimeout) throws DBException { + super(application.getApplicationInstanceId(), maxTimeout, GeneralUtils.getMetadataFolder()); } //avoid mockito access method error @Override - public void awaitingUnlock(String projectId, Path projectLockFile) throws InterruptedException, DBException { - super.awaitingUnlock(projectId, projectLockFile); + public void awaitingUnlock(@NotNull Path projectLockFile) throws InterruptedException, DBException { + super.awaitingUnlock(projectLockFile); } //avoid mockito access method error @Override - public boolean isLocked(Path lockFilePath) { + public boolean isLocked(@NotNull Path lockFilePath) { return super.isLocked(lockFilePath); } } diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java index 6dbf4f9e2b..b6c24f8886 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java @@ -1,64 +1,101 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.test.platform; +import io.cloudbeaver.CloudbeaverMockTest; +import io.cloudbeaver.app.CEAppStarter; +import io.cloudbeaver.auth.provider.local.LocalAuthProvider; import io.cloudbeaver.auth.provider.rp.RPAuthProvider; -import io.cloudbeaver.utils.WebTestUtils; +import io.cloudbeaver.test.WebGQLClient; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.auth.SMAuthStatus; import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.utils.SecurityUtils; import org.junit.Assert; -import org.junit.BeforeClass; import org.junit.Test; -import org.mockito.Mockito; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; -public class AuthenticationTest { - public static final String GQL_TEMPLATE_OPEN_SESSION = "openSession.json"; - public static final String GQL_TEMPLATE_ACTIVE_USER = "activeUser.json"; - public static final String REVERSE_PROXY_TEST_USER = "reverseProxyTestUser"; +public class AuthenticationTest extends CloudbeaverMockTest { + private static final String GQL_OPEN_SESSION = """ + mutation openSession($defaultLocale: String) { + result: openSession(defaultLocale: $defaultLocale) { + valid + } + }"""; + private static final String GQL_ACTIVE_USER = """ + query activeUser { + result: activeUser { + userId + } + }"""; + private static final String GQL_AUTH_LOGOUT = """ + query authLogoutExtended($provider: ID, $configuration: ID) { + result: authLogoutExtended(provider: $provider, configuration: $configuration) { + redirectLinks + } + }"""; @Test public void testLoginUser() throws Exception { - HttpClient client = CEServerTestSuite.createClient(); - Map authInfo = WebTestUtils.authenticateUser( - client, CEServerTestSuite.getScriptsPath(), CEServerTestSuite.GQL_API_URL); + WebGQLClient client = CEAppStarter.createClient(); + Map authInfo = CEAppStarter.authenticateTestUser(client); Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); } + @Test - public void testReverseProxyAnonymousModeLogin() throws Exception { - HttpClient client = CEServerTestSuite.createClient(); - Map sessionInfo = openSession(client); - Assert.assertTrue(JSONUtils.getBoolean(sessionInfo, "valid")); - Map activeUser = getActiveUser(client); - Assert.assertEquals(REVERSE_PROXY_TEST_USER, JSONUtils.getString(activeUser, "userId")); - } + public void testLoginUserWithCamelCase() throws Exception { + WebGQLClient client = CEAppStarter.createClient(); + for (String userId : Set.of("Test", "tESt", "tesT", "TEST")) { + Map credsWithCamelCase = getUserCredentials(userId); + // authenticating with user + Map authInfo = CEAppStarter.authenticateTestUser(client, credsWithCamelCase); + Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); + Map activeUser = client.sendQuery(GQL_ACTIVE_USER, null); + Assert.assertEquals(userId.toLowerCase(), JSONUtils.getString(activeUser, "userId")); + // making logout + client.sendQuery(GQL_AUTH_LOGOUT, Map.of("provider", "local")); - private Map openSession(HttpClient client) throws Exception { - Map data = doPostQuery(client, GQL_TEMPLATE_OPEN_SESSION); - if (data != null) { - return JSONUtils.getObject(data, "session"); + activeUser = client.sendQuery(GQL_ACTIVE_USER, null); + Assert.assertNotEquals(userId.toLowerCase(), JSONUtils.getString(activeUser, "userId")); } - return Collections.emptyMap(); } - private Map getActiveUser(HttpClient client) throws Exception { - Map data = doPostQuery(client, GQL_TEMPLATE_ACTIVE_USER); - if (data != null) { - return JSONUtils.getObject(data, "user"); - } - return Collections.emptyMap(); + @NotNull + private Map getUserCredentials(@NotNull String userId) throws Exception { + return Map.of( + LocalAuthProvider.CRED_USER, userId, + LocalAuthProvider.CRED_PASSWORD, SecurityUtils.makeDigest("test") + ); } - private Map doPostQuery(HttpClient client, String gqlScript) throws Exception { - String input = WebTestUtils.readScriptTemplate(gqlScript, CEServerTestSuite.getScriptsPath()); - List headers = List.of(RPAuthProvider.X_USER, REVERSE_PROXY_TEST_USER, RPAuthProvider.X_ROLE, "user"); - Map map = WebTestUtils.doPostWithHeaders(CEServerTestSuite.GQL_API_URL, input, client, headers); - return JSONUtils.getObjectOrNull(map, "data"); - } + @Test + public void testReverseProxyAnonymousModeLogin() throws Exception { + WebGQLClient client = CEAppStarter.createClient(); + String testUserId = "reverseProxyTestUser"; + List headers = List.of(RPAuthProvider.X_USER, testUserId, RPAuthProvider.X_TEAM, "user"); + Map sessionInfo = client.sendQueryWithHeaders(GQL_OPEN_SESSION, null, headers); + Assert.assertTrue(JSONUtils.getBoolean(sessionInfo, "valid")); + Map activeUser = client.sendQuery(GQL_ACTIVE_USER, null); + Assert.assertEquals(testUserId, JSONUtils.getString(activeUser, "userId")); + } } diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java index 9bd50a7fd8..f6ee4645b3 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2025 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. @@ -17,93 +17,35 @@ package io.cloudbeaver.test.platform; +import io.cloudbeaver.app.CEAppStarter; import io.cloudbeaver.model.rm.RMNIOTest; import io.cloudbeaver.model.rm.lock.RMLockTest; -import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBApplicationCE; -import io.cloudbeaver.utils.WebTestUtils; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.runner.RunWith; import org.junit.runners.Suite; -import java.net.CookieManager; -import java.net.http.HttpClient; -import java.nio.file.Path; - @RunWith(Suite.class) @Suite.SuiteClasses( { ConnectionsTest.class, + SQLQueryTranslatorTest.class, AuthenticationTest.class, ResourceManagerTest.class, RMLockTest.class, - RMNIOTest.class + RMNIOTest.class, + NoSessionTest.class } ) public class CEServerTestSuite { - public static final String GQL_API_URL = "http://localhost:18978/api/gql"; - public static final String SERVER_STATUS_URL = "http://localhost:18978/status"; - - private static boolean setUpIsDone = false; - private static boolean testFinished = false; - - private static CBApplication testApp; - private static HttpClient client; - private static Path scriptsPath; - private static Thread thread; - @BeforeClass public static void startServer() throws Exception { - if (setUpIsDone) { - return; - } else { - System.out.println("Start CBApplication"); - testApp = new CBApplicationCE(); - thread = new Thread(() -> { - testApp.start(null); - }); - thread.start(); - client = createClient(); - long startTime = System.currentTimeMillis(); - long endTime = 0; - while (true) { - setUpIsDone = WebTestUtils.getServerStatus(client, SERVER_STATUS_URL); - endTime = System.currentTimeMillis() - startTime; - if (setUpIsDone || endTime > 300000) { - break; - } - } - if (!setUpIsDone) { - throw new Exception("Server is not running"); - } - scriptsPath = Path.of(testApp.getHomeDirectory().toString(), "/workspace/gql_scripts") - .toAbsolutePath(); - } + CEAppStarter.startServerIfNotStarted(); } @AfterClass public static void shutdownServer() { - testApp.stop(); - } - - public static CBApplication getTestApp() { - return testApp; - } - - public static HttpClient getClient() { - return client; - } - - public static HttpClient createClient() { - return HttpClient.newBuilder() - .cookieHandler(new CookieManager()) - .version(HttpClient.Version.HTTP_2) - .build(); - } - - public static Path getScriptsPath() { - return scriptsPath; + CEAppStarter.shutdownServer(); } } diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/ConnectionsTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/ConnectionsTest.java index e67a3e5700..70a6839dcb 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/ConnectionsTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/ConnectionsTest.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,24 +17,38 @@ package io.cloudbeaver.test.platform; -import io.cloudbeaver.utils.WebTestUtils; +import io.cloudbeaver.CloudbeaverMockTest; +import io.cloudbeaver.app.CEAppStarter; +import io.cloudbeaver.test.WebGQLClient; +import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.utils.GeneralUtils; -import org.jkiss.utils.CommonUtils; +import org.junit.Assert; import org.junit.Test; -import java.net.http.HttpClient; import java.nio.file.Path; -import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -public class ConnectionsTest { - - public static final String GQL_TEMPLATE_CREATE_CONNECTION = "createConnection.json"; - public static final String GQL_TEMPLATE_DELETE_CONNECTION = "deleteConnection.json"; - public static final String GQL_TEMPLATE_USER_CONNECTIONS = "userConnections.json"; +public class ConnectionsTest extends CloudbeaverMockTest { + private static final String GQL_CONNECTIONS_GET = """ + query userConnections { + result: userConnections { + id + } + }"""; + private static final String GQL_CONNECTIONS_CREATE = """ + mutation createConnection($config: ConnectionConfig!, $projectId: ID) { + result: createConnection(config: $config, projectId: $projectId) { + id + } + }"""; + private static final String GQL_CONNECTIONS_DELETE = """ + mutation deleteConnection($id: ID!, $projectId: ID) { + result: deleteConnection(id: $id, projectId: $projectId) + }"""; @Test public void testAPlatformPresence() { @@ -52,50 +66,34 @@ public void testAPlatformPresence() { @Test public void testBCreateConnection() throws Exception { - HttpClient client = CEServerTestSuite.createClient(); - WebTestUtils.authenticateUser( - client, CEServerTestSuite.getScriptsPath(), CEServerTestSuite.GQL_API_URL); - - List> connections = getUserConnections(client); - Map addedConnection = createConnection(client); - if (!getUserConnections(client).contains(addedConnection)) { - throw new Exception("The new connection was not added"); - } + WebGQLClient client = CEAppStarter.createClient(); + CEAppStarter.authenticateTestUser(client); - boolean deleteConnection = deleteConnection(client, CommonUtils.toString(addedConnection.get("id"))); - if (!deleteConnection) { - throw new Exception("The new connection was not deleted"); - } - } + Map configuration = new LinkedHashMap<>(); + Map variables = new LinkedHashMap<>(); + variables.put("config", configuration); + Assert.assertThrows( + "Template connection or driver must be specified", + DBException.class, + () -> client.sendQuery(GQL_CONNECTIONS_CREATE, variables) + ); + String templateId = "test_template"; + configuration.put("templateId", templateId); + Assert.assertThrows( + "Template connection '" + templateId + "' not found", + DBException.class, + () -> client.sendQuery(GQL_CONNECTIONS_CREATE, variables) + ); - private List> getUserConnections(HttpClient client) throws Exception { - String input = WebTestUtils.readScriptTemplate(GQL_TEMPLATE_USER_CONNECTIONS, CEServerTestSuite.getScriptsPath()); - Map map = WebTestUtils.doPost(CEServerTestSuite.GQL_API_URL, input, client); - Map data = JSONUtils.getObjectOrNull(map, "data"); - if (data != null) { - return JSONUtils.getObjectList(data, "userConnections"); - } - return Collections.emptyList(); - } + configuration.remove("templateId"); + configuration.put("driverId", "postgresql:postgres-jdbc"); - private Map createConnection(HttpClient client) throws Exception { - String input = WebTestUtils.readScriptTemplate(GQL_TEMPLATE_CREATE_CONNECTION, CEServerTestSuite.getScriptsPath()); - Map map = WebTestUtils.doPost(CEServerTestSuite.GQL_API_URL, input, client); - Map data = JSONUtils.getObjectOrNull(map, "data"); - if (data != null) { - return JSONUtils.getObjectOrNull(data, "createConnection"); - } - return Collections.emptyMap(); - } + Map addedConnection = client.sendQuery(GQL_CONNECTIONS_CREATE, variables); - private boolean deleteConnection(HttpClient client, String connectionId) throws Exception { - String input = WebTestUtils.readScriptTemplate(GQL_TEMPLATE_DELETE_CONNECTION, CEServerTestSuite.getScriptsPath()) - .replace("${connectionId}", connectionId); - Map map = WebTestUtils.doPost(CEServerTestSuite.GQL_API_URL, input, client); - Map data = JSONUtils.getObjectOrNull(map, "data"); - if (data != null) { - return JSONUtils.getBoolean(data, "deleteConnection"); - } - return false; + List> connections = client.sendQuery(GQL_CONNECTIONS_GET, null); + Assert.assertTrue(connections.contains(addedConnection)); + String connectionId = JSONUtils.getString(addedConnection, "id"); + Assert.assertNotNull(connectionId); + Assert.assertTrue(client.sendQuery(GQL_CONNECTIONS_DELETE, Map.of("id", connectionId))); } } diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/NoSessionTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/NoSessionTest.java new file mode 100644 index 0000000000..bf7d4ff75d --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/NoSessionTest.java @@ -0,0 +1,49 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 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.test.platform; + +import io.cloudbeaver.CloudbeaverMockTest; +import io.cloudbeaver.app.CEAppStarter; +import org.junit.Assert; +import org.junit.Test; + +import java.net.CookieManager; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class NoSessionTest extends CloudbeaverMockTest { + + @Test + public void checkThatSessionNotCreatedWhenStaticResourcesCalled() throws Exception { + var cookieManager = new CookieManager(); + HttpClient httpClient = HttpClient.newBuilder() + .cookieHandler(cookieManager) + .version(HttpClient.Version.HTTP_2) + .build(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(CEAppStarter.SERVER_URL + "/favicon.ico")) + .GET() + .build(); + + httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + + Assert.assertTrue(cookieManager.getCookieStore().getCookies().isEmpty()); + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/ResourceManagerTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/ResourceManagerTest.java index 671903c6aa..443733587c 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/ResourceManagerTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/ResourceManagerTest.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2023 DBeaver Corp and others + * Copyright (C) 2010-2024 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. @@ -17,83 +17,106 @@ package io.cloudbeaver.test.platform; +import io.cloudbeaver.CloudbeaverMockTest; +import io.cloudbeaver.app.CEAppStarter; import io.cloudbeaver.model.rm.local.LocalResourceController; import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.utils.WebTestUtils; +import io.cloudbeaver.test.WebGQLClient; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.auth.SMAuthStatus; import org.jkiss.dbeaver.model.data.json.JSONUtils; -import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; -import java.net.CookieManager; -import java.net.http.HttpClient; +import java.nio.file.Path; import java.util.Map; -public class ResourceManagerTest { +public class ResourceManagerTest extends CloudbeaverMockTest { - public static final String GQL_TEMPLATE_RM_WRITE_RESOURCE = "rmWriteResource.json"; - public static final String GQL_TEMPLATE_RM_DELETE_RESOURCE = "rmDeleteResource.json"; - public static final String GQL_READ_EMPTY_PROJECT_ID_RESOURCES = "rmReadEmptyProjectIdResources.json"; - - private static HttpClient client; + private static WebGQLClient client; + private static final String GQL_RESOURCES_CREATE = """ + mutation rmWriteResourceStringContent($projectId: String!, $resourcePath: String!, $data: String!, $forceOverwrite: Boolean!) { + result: rmWriteResourceStringContent( + projectId: $projectId + resourcePath: $resourcePath + data: $data + forceOverwrite: $forceOverwrite + ) + }"""; + private static final String GQL_RESOURCES_DELETE = """ + mutation rmDeleteResource($projectId: String!, $resourcePath: String!, $recursive: Boolean!) { + result: rmDeleteResource( + projectId: $projectId + resourcePath: $resourcePath + recursive: $recursive + ) + }"""; + private static final String GQL_RESOURCES_LIST = """ + query rmListResources($projectId: String!, $folder: String, $nameMask: String, $readProperties: Boolean, $readHistory: Boolean) { + result: rmListResources( + projectId: $projectId + folder: $folder + nameMask: $nameMask + readProperties: $readProperties + readHistory: $readHistory + ) { + name + folder + } + }"""; @BeforeClass public static void init() throws Exception { Assert.assertTrue(CBApplication.getInstance().getAppConfiguration().isResourceManagerEnabled()); - client = HttpClient.newBuilder() - .cookieHandler(new CookieManager()) - .version(HttpClient.Version.HTTP_2) - .build(); - Map authInfo = WebTestUtils.authenticateUser( - client, CEServerTestSuite.getScriptsPath(), CEServerTestSuite.GQL_API_URL); + client = CEAppStarter.createClient(); + Map authInfo = CEAppStarter.authenticateTestUser(client); Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); - } @Test public void createDeleteResourceTest() throws Exception { - Assert.assertTrue(createResource(client, false)); - Assert.assertFalse(createResource(client, false)); - Assert.assertTrue(createResource(client, true)); - Assert.assertTrue(deleteResource(client)); + String projectId = "u_test"; + String resourcePath = "testScript.sql"; + Assert.assertTrue(createResource(projectId, resourcePath, false)); + Assert.assertThrows( + "Resource '" + IOUtils.getFileNameWithoutExtension(Path.of(resourcePath)) + "' already exists", + DBException.class, + () -> createResource(projectId, resourcePath, false) + ); + Assert.assertTrue(createResource(projectId, resourcePath, true)); + Assert.assertTrue(deleteResource(projectId, resourcePath)); } @Test public void listResourcesWithInvalidProjectId() throws Exception { - String input = WebTestUtils.readScriptTemplate(GQL_READ_EMPTY_PROJECT_ID_RESOURCES, CEServerTestSuite.getScriptsPath()); - Map map = WebTestUtils.doPost(CEServerTestSuite.GQL_API_URL, input, client); - var errors = JSONUtils.getObjectList(map, "errors"); - Assert.assertFalse("No errors happened with empty project id request", errors.isEmpty()); - var rmError = errors.get(0); - //FIXME stupid way to validate error - Assert.assertTrue(JSONUtils.getString(rmError, "message", "").contains("Project id is empty")); + Assert.assertThrows( + "Project id is empty", + DBException.class, + () -> client.sendQuery(GQL_RESOURCES_LIST, Map.of("projectId", "")) + ); } - private boolean createResource(HttpClient client, boolean forceOverwrite) throws Exception { - String input = WebTestUtils.readScriptTemplate( - GQL_TEMPLATE_RM_WRITE_RESOURCE, CEServerTestSuite.getScriptsPath() - ).replaceAll("\\{forceOverwrite}", CommonUtils.toString(forceOverwrite)); - Map map = WebTestUtils.doPost(CEServerTestSuite.GQL_API_URL, input, client); - Map data = JSONUtils.getObjectOrNull(map, "data"); - if (data != null) { - return LocalResourceController.DEFAULT_CHANGE_ID.equals(JSONUtils.getString(data, "rmWriteResourceStringContent")); - } - return false; + private boolean createResource(@NotNull String projectId, @NotNull String resourcePath, boolean forceOverwrite) throws Exception { + Map variables = Map.of( + "projectId", projectId, + "resourcePath", resourcePath, + "data", "TEST SCRIPT", + "forceOverwrite", forceOverwrite + ); + String data = client.sendQuery(GQL_RESOURCES_CREATE, variables); + return LocalResourceController.DEFAULT_CHANGE_ID.equals(data); } - private boolean deleteResource(HttpClient client) throws Exception { - String input = WebTestUtils.readScriptTemplate(GQL_TEMPLATE_RM_DELETE_RESOURCE, CEServerTestSuite.getScriptsPath()); - Map map = WebTestUtils.doPost(CEServerTestSuite.GQL_API_URL, input, client); - Map data = JSONUtils.getObjectOrNull(map, "data"); - if (map.containsKey("errors")) { - return false; - } - if (data != null) { - return data.containsKey("rmDeleteResource"); - } - return false; + private boolean deleteResource(@NotNull String projectId, @NotNull String resourcePath) throws Exception { + Map variables = Map.of( + "projectId", projectId, + "resourcePath", resourcePath, + "recursive", false + ); + return client.sendQuery(GQL_RESOURCES_DELETE, variables); } } diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/SQLQueryTranslatorTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/SQLQueryTranslatorTest.java new file mode 100644 index 0000000000..d4c4c28746 --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/SQLQueryTranslatorTest.java @@ -0,0 +1,209 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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.test.platform; + +import io.cloudbeaver.CloudbeaverMockTest; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.ext.h2.model.H2SQLDialect; +import org.jkiss.dbeaver.ext.mssql.model.SQLServerDialect; +import org.jkiss.dbeaver.ext.mysql.model.MySQLDialect; +import org.jkiss.dbeaver.ext.oracle.model.OracleSQLDialect; +import org.jkiss.dbeaver.ext.postgresql.model.PostgreDialect; +import org.jkiss.dbeaver.model.impl.sql.BasicSQLDialect; +import org.jkiss.dbeaver.model.sql.SQLDialect; +import org.jkiss.dbeaver.model.sql.translate.SQLQueryTranslator; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class SQLQueryTranslatorTest extends CloudbeaverMockTest { + @Test + public void createSimpleTable() throws DBException { + var basicSql = "CREATE TABLE CB_AUTH_SUBJECT (SUBJECT_ID VARCHAR(128) NOT NULL," + + " SUBJECT_TYPE VARCHAR(8) NOT NULL," + + " IS_SECRET_STORAGE CHAR(1) DEFAULT 'Y' NOT NULL," + + " PRIMARY KEY (SUBJECT_ID));\n"; + + Map expectedSqlByDialect = new HashMap<>(); + expectedSqlByDialect.put(new H2SQLDialect(), basicSql); + expectedSqlByDialect.put( + new PostgreDialect(), + "CREATE TABLE CB_AUTH_SUBJECT (SUBJECT_ID VARCHAR (128) NOT NULL,\n" + + "SUBJECT_TYPE VARCHAR (8) NOT NULL,\n" + + "IS_SECRET_STORAGE CHAR (1) DEFAULT 'Y' NOT NULL,\n" + + "PRIMARY KEY (SUBJECT_ID));\n" + ); + expectedSqlByDialect.put(new MySQLDialect(), + "CREATE TABLE CB_AUTH_SUBJECT (SUBJECT_ID VARCHAR (128) NOT NULL,\n" + + "SUBJECT_TYPE VARCHAR (8) NOT NULL,\n" + + "IS_SECRET_STORAGE CHAR (1) DEFAULT 'Y' NOT NULL,\n" + + "PRIMARY KEY (SUBJECT_ID));\n"); + expectedSqlByDialect.put(new OracleSQLDialect(), basicSql); + expectedSqlByDialect.put(new SQLServerDialect(), basicSql); + + translateAndValidateQueries(basicSql, expectedSqlByDialect); + } + + @Test + public void addColumn() throws DBException { + var basicSql = "ALTER TABLE CB_AUTH_ATTEMPT ADD ERROR_CODE VARCHAR(128) NULL;\n"; + //same for all dialects + Map expectedSqlByDialect = new HashMap<>(); + expectedSqlByDialect.put(new H2SQLDialect(), basicSql); + expectedSqlByDialect.put(new PostgreDialect(), basicSql); + expectedSqlByDialect.put(new MySQLDialect(), basicSql); + expectedSqlByDialect.put(new OracleSQLDialect(), basicSql); + expectedSqlByDialect.put(new SQLServerDialect(), basicSql); + + translateAndValidateQueries(basicSql, expectedSqlByDialect); + } + + @Test + public void alterColumn() throws DBException { + var basicSql = "ALTER TABLE CB_TABLE ALTER COLUMN CB_COLUMN SET NULL;\n"; + + // we use custom scripts for postgres/mysql/oracle + Map expectedSqlByDialect = new HashMap<>(); + expectedSqlByDialect.put(new H2SQLDialect(), basicSql); + expectedSqlByDialect.put(new OracleSQLDialect(), "ALTER TABLE CB_TABLE MODIFY CB_COLUMN NULL;\n"); + + translateAndValidateQueries(basicSql, expectedSqlByDialect); + } + + @Test + public void createTableWithUuid() throws DBException { + var basicSql = "CREATE TABLE CB_TEST_TYPES (UUID_COLUMN UUID);\n"; + + Map expectedSqlByDialect = new HashMap<>(); + expectedSqlByDialect.put(new H2SQLDialect(), basicSql); + expectedSqlByDialect.put(new PostgreDialect(), basicSql); + expectedSqlByDialect.put( + new MySQLDialect(), + "CREATE TABLE CB_TEST_TYPES (UUID_COLUMN CHAR(36));\n" + ); + expectedSqlByDialect.put(new OracleSQLDialect(), "CREATE TABLE CB_TEST_TYPES (UUID_COLUMN VARCHAR2(36));\n"); + expectedSqlByDialect.put( + new SQLServerDialect(), + "CREATE TABLE CB_TEST_TYPES (UUID_COLUMN UNIQUEIDENTIFIER);\n" + ); + translateAndValidateQueries(basicSql, expectedSqlByDialect); + } + + @Test + public void createTableWithBoolean() throws DBException { + var basicSql = "CREATE TABLE CB_TEST_TYPES (BOOLEAN_COLUMN BOOLEAN);\n"; + + Map expectedSqlByDialect = new HashMap<>(); + expectedSqlByDialect.put(new H2SQLDialect(), basicSql); + expectedSqlByDialect.put(new PostgreDialect(), basicSql); + expectedSqlByDialect.put(new MySQLDialect(), "CREATE TABLE CB_TEST_TYPES (BOOLEAN_COLUMN TINYINT(1));\n"); + + expectedSqlByDialect.put( + new OracleSQLDialect(), + "CREATE TABLE CB_TEST_TYPES (BOOLEAN_COLUMN VARCHAR(1));\n" + ); + expectedSqlByDialect.put( + new SQLServerDialect(), + "CREATE TABLE CB_TEST_TYPES (BOOLEAN_COLUMN BIT);\n" + ); + translateAndValidateQueries(basicSql, expectedSqlByDialect); + } + + @Test + public void createTableWithBlob() throws DBException { + var basicSql = "CREATE TABLE CB_TEST_TYPES (BLOB_COLUMN BLOB);\n"; + + Map expectedSqlByDialect = new HashMap<>(); + expectedSqlByDialect.put(new H2SQLDialect(), basicSql); + expectedSqlByDialect.put(new PostgreDialect(), "CREATE TABLE CB_TEST_TYPES (BLOB_COLUMN BYTEA);\n"); + expectedSqlByDialect.put(new MySQLDialect(), basicSql); + + expectedSqlByDialect.put(new OracleSQLDialect(), basicSql); + expectedSqlByDialect.put(new SQLServerDialect(), "CREATE TABLE CB_TEST_TYPES (BLOB_COLUMN IMAGE);\n"); + translateAndValidateQueries(basicSql, expectedSqlByDialect); + } + + @Test + public void createTableWithBigint() throws DBException { + var basicSql = "CREATE TABLE CB_TEST_TYPES (BIGINT_COLUMN BIGINT);\n"; + + Map expectedSqlByDialect = new HashMap<>(); + expectedSqlByDialect.put(new H2SQLDialect(), basicSql); + expectedSqlByDialect.put(new PostgreDialect(), basicSql); + expectedSqlByDialect.put(new MySQLDialect(), basicSql); + + expectedSqlByDialect.put(new OracleSQLDialect(), "CREATE TABLE CB_TEST_TYPES (BIGINT_COLUMN NUMBER);\n"); + expectedSqlByDialect.put(new SQLServerDialect(), basicSql); + translateAndValidateQueries(basicSql, expectedSqlByDialect); + } + + @Test + public void createTableWithAutoincrement() throws DBException { + var basicSql = "CREATE TABLE CB_TEST_TYPES (AUTOINC_COLUMN BIGINT AUTO_INCREMENT NOT NULL);\n"; + + Map expectedSqlByDialect = new HashMap<>(); + expectedSqlByDialect.put(new H2SQLDialect(), basicSql); + expectedSqlByDialect.put(new PostgreDialect(), "CREATE SEQUENCE CB_TEST_TYPES_AUTOINC_COLUMN;\n" + + "CREATE TABLE CB_TEST_TYPES (AUTOINC_COLUMN BIGINT NOT NULL DEFAULT NEXTVAL" + + "('CB_TEST_TYPES_AUTOINC_COLUMN'));\n" + + "ALTER SEQUENCE CB_TEST_TYPES_AUTOINC_COLUMN OWNED BY CB_TEST_TYPES.AUTOINC_COLUMN;\n" + ); + expectedSqlByDialect.put(new MySQLDialect(), basicSql); + + expectedSqlByDialect.put( + new OracleSQLDialect(), + "CREATE TABLE CB_TEST_TYPES (AUTOINC_COLUMN NUMBER GENERATED ALWAYS AS IDENTITY NOT NULL);\n"); + expectedSqlByDialect.put( + new SQLServerDialect(), + "CREATE TABLE CB_TEST_TYPES (AUTOINC_COLUMN BIGINT IDENTITY NOT NULL);\n"); + translateAndValidateQueries(basicSql, expectedSqlByDialect); + + } + + private static void translateAndValidateQueries( + @NotNull String basicSql, + @NotNull Map expectedSqlByDialect + ) throws DBException { + var preferenceStore = DBWorkbench.getPlatform().getPreferenceStore(); + SQLDialect sourceDialect = new BasicSQLDialect() { + }; + for (Map.Entry entry : expectedSqlByDialect.entrySet()) { + String translated = SQLQueryTranslator.translateScript( + sourceDialect, + entry.getKey(), + preferenceStore, + basicSql + ); + Assert.assertEquals( + entry.getKey().getDialectId() + " has invalid syntax " + translated, + normalizeScript(entry.getValue()), + normalizeScript(translated) + ); + } + } + + private static String normalizeScript(String script) { + // Unify for tests + return script.toLowerCase().replace(GeneralUtils.getDefaultLineSeparator(), "\n"); + } + +} diff --git a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf index ba7e5de3b3..6d2cf2a19c 100644 --- a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf +++ b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf @@ -1,43 +1,47 @@ { server: { serverPort: "${CLOUDBEAVER_TEST_PORT:18978}", - serverName: "CloudBeaver CE Test Server", + serverName: "${CLOUDBEAVER_SERVER_NAME:CloudBeaver CE Test Server}", - workspaceLocation: "workspace", contentRoot: "workspace/web", driversLocation: "../../../deploy/", - rootURI: "/", + rootURI: "${CLOUDBEAVER_ROOT_URI:/}", serviceURI: "/api/", - productConfiguration: "workspace/conf/product.conf", + productSettings: {}, - expireSessionAfterPeriod: 1800000, + expireSessionAfterPeriod: "${CLOUDBEAVER_EXPIRE_SESSION_AFTER_PERIOD:1800000}", - develMode: false, + develMode: "${CLOUDBEAVER_DEVEL_MODE:false}", + + sm: { + enableBruteForceProtection: "${CLOUDBEAVER_BRUTE_FORCE_PROTECTION_ENABLED:false}" + }, database: { - driver="h2_embedded_v2", - url: "jdbc:h2:mem:testdb", + driver: "${CLOUDBEAVER_DB_DRIVER:h2_embedded_v2}", + url: "${CLOUDBEAVER_DB_URL:jdbc:h2:mem:testdb}", - createDatabase: true, + createDatabase: "${CLOUDBEAVER_CREATE_DATABASE:true}", - initialDataConfiguration: "workspace/conf/initial-data.conf", + initialDataConfiguration: "${CLOUDBEAVER_DB_INITIAL_DATA:workspace/conf/initial-data.conf}", pool: { - minIdleConnections: 4, - maxIdleConnections: 10, - maxConnections: 100, - validationQuery: "SELECT 1" + minIdleConnections: "${CLOUDBEAVER_DB_MIN_IDLE_CONNECTIONS:4}", + maxIdleConnections: "${CLOUDBEAVER_DB_MAX_IDLE_CONNECTIONS:10}", + maxConnections: "${CLOUDBEAVER_DB_MAX_CONNECTIONS:100}", + validationQuery: "${CLOUDBEAVER_DB_VALIDATION_QUERY:SELECT 1}" } } }, app: { - anonymousAccessEnabled: true, - anonymousUserRole: "user", - supportsCustomConnections: true, - enableReverseProxyAuth: true, + anonymousAccessEnabled: "${CLOUDBEAVER_APP_ANONYMOUS_ACCESS_ENABLED:true}", + anonymousUserRole: user, + defaultUserTeam: "${CLOUDBEAVER_APP_DEFAULT_USER_TEAM:user}", + supportsCustomConnections: "${CLOUDBEAVER_APP_SUPPORTS_CUSTOM_CONNECTIONS:true}", + enableReverseProxyAuth: "${CLOUDBEAVER_APP_ENABLE_REVERSE_PROXY_AUTH:true}", enabledAuthProviders: [ "local", "reverseProxy" @@ -47,40 +51,18 @@ ], resourceQuotas: { - dataExportFileSizeLimit: 10000000, - sqlMaxRunningQueries: 100, - sqlResultSetRowsLimit: 100000, - sqlResultSetMemoryLimit: 2000000, - sqlTextPreviewMaxLength: 4096, - sqlBinaryPreviewMaxLength: 261120, - sqlQueryTimeout: 5 + dataExportFileSizeLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_DATA_EXPORT_FILE_SIZE_LIMIT:10000000}", + sqlMaxRunningQueries: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_MAX_RUNNING_QUERIES:100}", + sqlResultSetRowsLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_RESULT_SET_ROWS_LIMIT:100000}", + sqlTextPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_TEXT_PREVIEW_MAX_LENGTH:4096}", + sqlBinaryPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_BINARY_PREVIEW_MAX_LENGTH:261120}", + sqlQueryTimeout: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_QUERY_TIMEOUT:5}" }, disabledDrivers: [ "sqlite:sqlite_jdbc", "h2:h2_embedded", "h2:h2_embedded_v2" - ], - - plugins: { - aws: { - clouds: { - AWS: { - cloudName: "AWS", - - autoRegistrationEnabled: true, - autoRegistrationAccounts: [ ], - defaultRegions: [ ], - enabledServices: [ "rds", "redshift", "dynamodb", "documentdb" ], - - federatedAccessEnabled: true - } - } - }, - saml: { - signon-finish-uri = "/sso.html", - signout-finish-uri = "/sso.html" - } - } + ] } } diff --git a/server/test/io.cloudbeaver.test.platform/workspace/conf/initial-data.conf b/server/test/io.cloudbeaver.test.platform/workspace/conf/initial-data.conf index b23e2346c4..fa63f4640a 100644 --- a/server/test/io.cloudbeaver.test.platform/workspace/conf/initial-data.conf +++ b/server/test/io.cloudbeaver.test.platform/workspace/conf/initial-data.conf @@ -4,14 +4,14 @@ teams: [ { subjectId: "admin", - name: "Admin", + teamName: "Admin", description: "Administrative access. Has all permissions.", permissions: [ "admin" ] }, { subjectId: "user", - name: "User", - description: "Standard user", + teamName: "User", + description: "All users, including anonymous.", permissions: [ ] } ] diff --git a/server/test/io.cloudbeaver.test.platform/workspace/conf/product.conf b/server/test/io.cloudbeaver.test.platform/workspace/conf/product.conf deleted file mode 100644 index b834cd326b..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/conf/product.conf +++ /dev/null @@ -1,42 +0,0 @@ -// Product configuration. Customized web application behavior -// It is in JSONC format -{ - // Global properties - core: { - // User defaults - user: { - defaultTheme: "light", - defaultLanguage: "en" - }, - app: { - // Log viewer config - logViewer: { - refreshTimeout: 3000, - logBatchSize: 1000, - maxLogRecords: 2000, - maxFailedRequests: 3 - }, - sqlEditor: { - # max size of the file that can be uploaded to the editor (in kilobytes) - maxFileSize: 100 - } - }, - authentication: { - primaryAuthProvider: 'local' - } - }, - // Notifications config - core_events: { - notificationsPool: 5 - }, - plugin_data_spreadsheet_new: { - hidden: false - }, - plugin_data_export: { - disabled: false - }, - // ERD config - plugin_erd_viewer: { - maxColumnsToDisplay: 7500 - } -} diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/activeUser.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/activeUser.json deleted file mode 100644 index 97b079675a..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/activeUser.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "query": "\n query activeUser {\n user: activeUser {\n userId\n }\n}\n ", - "operationName": "activeUser" -} \ No newline at end of file diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/authLogin.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/authLogin.json deleted file mode 100644 index c0d1796e74..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/authLogin.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "query": "\n query authLogin($provider: ID!, $configuration: ID, $credentials: Object, $linkUser: Boolean) {\n authInfo: authLogin(\n provider: $provider\n configuration: $configuration\n credentials: $credentials\n linkUser: $linkUser\n ) {\n authStatus\n }\n}\n ", - "variables": { - "provider": "local", - "credentials": { - "user": "test", - "password": "098F6BCD4621D373CADE4E832627B4F6" - }, - "linkUser": true - }, - "operationName": "authLogin" -} \ No newline at end of file diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/createConnection.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/createConnection.json deleted file mode 100644 index a9340bb121..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/createConnection.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "query": "\n mutation createConnection($projectId:ID!, $config: ConnectionConfig!) {\n createConnection(projectId:$projectId, config: $config) {\n id\n }\n} ", - "variables": { - "projectId": "u_test", - "config": { - "name": "PostgreSQL@localhost (1)", - "driverId": "postgresql:postgres-jdbc", - "host": "localhost", - "port": "5432", - "databaseName": "postgres", - "authModelId": "native", - "saveCredentials": true, - "credentials": { - "userName": "postgres", - "userPassword": "postgres" - }, - "providerProperties": {}, - "properties": {} - } - }, - "operationName": "createConnection" -} \ No newline at end of file diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/deleteConnection.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/deleteConnection.json deleted file mode 100644 index 21837ecdc6..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/deleteConnection.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "query": "mutation deleteConnection($projectId:ID!, $id: ID!) {deleteConnection(projectId:$projectId, id: $id)}", - "variables": { - "projectId": "u_test", - "id": "${connectionId}" - }, - "operationName": "deleteConnection" -} \ No newline at end of file diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/navDeleteNode.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/navDeleteNode.json deleted file mode 100644 index 7302133c3d..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/navDeleteNode.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "query": "\n mutation navDeleteNodes($nodePaths: [ID!]!) {\n navDeleteNodes(nodePaths: $nodePaths)\n}\n ", - "variables": { - "nodePaths": [ - "ext://resources/u_test/testScript.sql" - ] - }, - "operationName": "navDeleteNodes" -} \ No newline at end of file diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/openSession.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/openSession.json deleted file mode 100644 index eff24dcf81..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/openSession.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "query": "\n mutation openSession($defaultLocale: String) {\n session: openSession(defaultLocale: $defaultLocale) {\n valid\n }\n}\n ", - "variables": { - "defaultLocale": "en" - }, - "operationName": "openSession" -} \ No newline at end of file diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmDeleteResource.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmDeleteResource.json deleted file mode 100644 index f820b6222c..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmDeleteResource.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "query": "\n mutation rmDeleteResource($projectId: String!, $resourcePath: String!, $recursive: Boolean!) {\n rmDeleteResource(\n projectId: $projectId\n resourcePath: $resourcePath\n recursive: $recursive\n )\n}\n ", - "variables": { - "projectId": "u_test", - "resourcePath": "testScript.sql", - "recursive": false - }, - "operationName": "rmDeleteResource" -} \ No newline at end of file diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmReadEmptyProjectIdResources.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmReadEmptyProjectIdResources.json deleted file mode 100644 index 114f2ea64c..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmReadEmptyProjectIdResources.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "query": "query {\n rmListResources(projectId: \"\") {\n name\n folder\n}\n}" -} \ No newline at end of file diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmWriteResource.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmWriteResource.json deleted file mode 100644 index 1652331d3b..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/rmWriteResource.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "query": "\n mutation writeContent($projectId: String!, $resourcePath: String!, $data: String!, $forceOverwrite: Boolean!) {\n rmWriteResourceStringContent(\n projectId: $projectId\n resourcePath: $resourcePath\n data: $data\n forceOverwrite: $forceOverwrite\n )\n}\n ", - "variables": { - "projectId": "u_test", - "resourcePath": "testScript.sql", - "data": "TEST SCRIPT;", - "forceOverwrite": {forceOverwrite} - }, - "operationName": "writeContent" -} \ No newline at end of file diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/userConnections.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/userConnections.json deleted file mode 100644 index 5859aa23a7..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/userConnections.json +++ /dev/null @@ -1,5 +0,0 @@ -{"query": "query userConnections($id: ID) {userConnections(id: $id) { id}}", -"variables": { - "id" : null -}, -"operationName":"userConnections"} \ No newline at end of file diff --git a/server/test/pom.xml b/server/test/pom.xml index 1e8d711c59..26b4da16e8 100644 --- a/server/test/pom.xml +++ b/server/test/pom.xml @@ -17,6 +17,11 @@ io.cloudbeaver.test.platform + + + + + @@ -28,7 +33,12 @@ eclipse-plugin - ch.qos.logback.slf4j + ch.qos.logback.classic + 0.0.0 + + + eclipse-feature + io.cloudbeaver.ws.feature 0.0.0 @@ -50,7 +60,10 @@ default io.cloudbeaver.test.platform.CEServerTestSuite -web-config workspace/conf/cloudbeaver.conf - + + target/workspace + + -Dlogback.configurationFile=workspace/conf/logback.xml ${debugArgs} diff --git a/webapp/.eslintignore b/webapp/.eslintignore deleted file mode 100644 index eadad9960d..0000000000 --- a/webapp/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -**/node_modules -**/lib diff --git a/webapp/.eslintrc.cjs b/webapp/.eslintrc.cjs deleted file mode 100644 index c22fd15204..0000000000 --- a/webapp/.eslintrc.cjs +++ /dev/null @@ -1,19 +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. - */ - -module.exports = { - root: true, - extends: '@cloudbeaver', - parserOptions: { - ecmaVersion: 2019, - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - }, -}; \ No newline at end of file diff --git a/webapp/.gitattributes b/webapp/.gitattributes new file mode 100644 index 0000000000..af3ad12812 --- /dev/null +++ b/webapp/.gitattributes @@ -0,0 +1,4 @@ +/.yarn/** linguist-vendored +/.yarn/releases/* binary +/.yarn/plugins/**/* binary +/.pnp.* binary linguist-generated diff --git a/webapp/.gitignore b/webapp/.gitignore index d9ea6b69ed..d999b11b8a 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -1,11 +1,23 @@ -.idea node_modules -src/**/*.js -src/**/*.js.map -typings dist lib coverage -ts-out npm-debug.log -debug.log \ No newline at end of file +debug.log +yarn-error.log + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +!.yarn/sdks/** + +# Swap the comments on the following lines if you wish to use zero-installs +# In that case, don't forget to run `yarn config set enableGlobalCache false`! +# Documentation here: https://yarnpkg.com/features/caching#zero-installs + +#!.yarn/cache +.pnp.* \ No newline at end of file diff --git a/webapp/.prettierrc.mjs b/webapp/.prettierrc.mjs new file mode 100644 index 0000000000..de21a410e9 --- /dev/null +++ b/webapp/.prettierrc.mjs @@ -0,0 +1,10 @@ +import defaultDbeaverConfig from '@dbeaver/prettier-config'; + +/** + * @type {import("prettier").Config} + */ +const config = { + ...defaultDbeaverConfig, +}; + +export default config; diff --git a/webapp/.vscode/extensions.json b/webapp/.vscode/extensions.json new file mode 100644 index 0000000000..daaa5ee2ec --- /dev/null +++ b/webapp/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "arcanis.vscode-zipfs", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] +} diff --git a/webapp/.vscode/settings.json b/webapp/.vscode/settings.json new file mode 100644 index 0000000000..b7e02b5c94 --- /dev/null +++ b/webapp/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "search.exclude": { + "**/.yarn": true, + "**/.pnp.*": true + }, + "eslint.nodePath": ".yarn/sdks", + "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs", + "typescript.tsdk": ".yarn/sdks/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} diff --git a/webapp/.yarn/plugins/@yarnpkg/plugin-ts-project-linker.cjs b/webapp/.yarn/plugins/@yarnpkg/plugin-ts-project-linker.cjs new file mode 100644 index 0000000000..39e5a79818 --- /dev/null +++ b/webapp/.yarn/plugins/@yarnpkg/plugin-ts-project-linker.cjs @@ -0,0 +1,74 @@ +/* eslint-disable */ +//prettier-ignore +module.exports = { +name: "@yarnpkg/plugin-ts-project-linker", +factory: function (require) { +var plugin=(()=>{var vl=Object.create;var kt=Object.defineProperty;var Sl=Object.getOwnPropertyDescriptor;var Al=Object.getOwnPropertyNames;var Cl=Object.getPrototypeOf,bl=Object.prototype.hasOwnProperty;var O=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,r)=>(typeof require<"u"?require:e)[r]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var _=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),Fl=(t,e)=>{for(var r in e)kt(t,r,{get:e[r],enumerable:!0})},as=(t,e,r,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of Al(e))!bl.call(t,a)&&a!==r&&kt(t,a,{get:()=>e[a],enumerable:!(i=Sl(e,a))||i.enumerable});return t};var ne=(t,e,r)=>(r=t!=null?vl(Cl(t)):{},as(e||!t||!t.__esModule?kt(r,"default",{value:t,enumerable:!0}):r,t)),wl=t=>as(kt({},"__esModule",{value:!0}),t);var ls=_((Z0,Fr)=>{var Bt=process||{},us=Bt.argv||[],Tt=Bt.env||{},_l=!(Tt.NO_COLOR||us.includes("--no-color"))&&(!!Tt.FORCE_COLOR||us.includes("--color")||Bt.platform==="win32"||(Bt.stdout||{}).isTTY&&Tt.TERM!=="dumb"||!!Tt.CI),kl=(t,e,r=t)=>i=>{let a=""+i,c=a.indexOf(e,t.length);return~c?t+Tl(a,e,r,c)+e:t+a+e},Tl=(t,e,r,i)=>{let a="",c=0;do a+=t.substring(c,i)+r,c=i+e.length,i=t.indexOf(e,c);while(~i);return a+t.substring(c)},cs=(t=_l)=>{let e=t?kl:()=>String;return{isColorSupported:t,reset:e("\x1B[0m","\x1B[0m"),bold:e("\x1B[1m","\x1B[22m","\x1B[22m\x1B[1m"),dim:e("\x1B[2m","\x1B[22m","\x1B[22m\x1B[2m"),italic:e("\x1B[3m","\x1B[23m"),underline:e("\x1B[4m","\x1B[24m"),inverse:e("\x1B[7m","\x1B[27m"),hidden:e("\x1B[8m","\x1B[28m"),strikethrough:e("\x1B[9m","\x1B[29m"),black:e("\x1B[30m","\x1B[39m"),red:e("\x1B[31m","\x1B[39m"),green:e("\x1B[32m","\x1B[39m"),yellow:e("\x1B[33m","\x1B[39m"),blue:e("\x1B[34m","\x1B[39m"),magenta:e("\x1B[35m","\x1B[39m"),cyan:e("\x1B[36m","\x1B[39m"),white:e("\x1B[37m","\x1B[39m"),gray:e("\x1B[90m","\x1B[39m"),bgBlack:e("\x1B[40m","\x1B[49m"),bgRed:e("\x1B[41m","\x1B[49m"),bgGreen:e("\x1B[42m","\x1B[49m"),bgYellow:e("\x1B[43m","\x1B[49m"),bgBlue:e("\x1B[44m","\x1B[49m"),bgMagenta:e("\x1B[45m","\x1B[49m"),bgCyan:e("\x1B[46m","\x1B[49m"),bgWhite:e("\x1B[47m","\x1B[49m"),blackBright:e("\x1B[90m","\x1B[39m"),redBright:e("\x1B[91m","\x1B[39m"),greenBright:e("\x1B[92m","\x1B[39m"),yellowBright:e("\x1B[93m","\x1B[39m"),blueBright:e("\x1B[94m","\x1B[39m"),magentaBright:e("\x1B[95m","\x1B[39m"),cyanBright:e("\x1B[96m","\x1B[39m"),whiteBright:e("\x1B[97m","\x1B[39m"),bgBlackBright:e("\x1B[100m","\x1B[49m"),bgRedBright:e("\x1B[101m","\x1B[49m"),bgGreenBright:e("\x1B[102m","\x1B[49m"),bgYellowBright:e("\x1B[103m","\x1B[49m"),bgBlueBright:e("\x1B[104m","\x1B[49m"),bgMagentaBright:e("\x1B[105m","\x1B[49m"),bgCyanBright:e("\x1B[106m","\x1B[49m"),bgWhiteBright:e("\x1B[107m","\x1B[49m")}};Fr.exports=cs();Fr.exports.createColors=cs});var hs=_((dt,wr)=>{(function(e,r){typeof dt=="object"&&typeof wr=="object"?wr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof dt=="object"?dt.esprima=r():e.esprima=r()})(dt,function(){return function(t){var e={};function r(i){if(e[i])return e[i].exports;var a=e[i]={exports:{},id:i,loaded:!1};return t[i].call(a.exports,a,a.exports,r),a.loaded=!0,a.exports}return r.m=t,r.c=e,r.p="",r(0)}([function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(1),a=r(3),c=r(8),u=r(15);function x(l,n,s){var o=null,f=function(P,N){s&&s(P,N),o&&o.visit(P,N)},E=typeof s=="function"?f:null,g=!1;if(n){g=typeof n.comment=="boolean"&&n.comment;var v=typeof n.attachComment=="boolean"&&n.attachComment;(g||v)&&(o=new i.CommentHandler,o.attach=v,n.comment=!0,E=f)}var F=!1;n&&typeof n.sourceType=="string"&&(F=n.sourceType==="module");var A;n&&typeof n.jsx=="boolean"&&n.jsx?A=new a.JSXParser(l,n,E):A=new c.Parser(l,n,E);var T=F?A.parseModule():A.parseScript(),k=T;return g&&o&&(k.comments=o.comments),A.config.tokens&&(k.tokens=A.tokens),A.config.tolerant&&(k.errors=A.errorHandler.errors),k}e.parse=x;function d(l,n,s){var o=n||{};return o.sourceType="module",x(l,o,s)}e.parseModule=d;function p(l,n,s){var o=n||{};return o.sourceType="script",x(l,o,s)}e.parseScript=p;function h(l,n,s){var o=new u.Tokenizer(l,n),f;f=[];try{for(;;){var E=o.getNextToken();if(!E)break;s&&(E=s(E)),f.push(E)}}catch(g){o.errorHandler.tolerate(g)}return o.errorHandler.tolerant&&(f.errors=o.errors()),f}e.tokenize=h;var m=r(2);e.Syntax=m.Syntax,e.version="4.0.1"},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(2),a=function(){function c(){this.attach=!1,this.comments=[],this.stack=[],this.leading=[],this.trailing=[]}return c.prototype.insertInnerComments=function(u,x){if(u.type===i.Syntax.BlockStatement&&u.body.length===0){for(var d=[],p=this.leading.length-1;p>=0;--p){var h=this.leading[p];x.end.offset>=h.start&&(d.unshift(h.comment),this.leading.splice(p,1),this.trailing.splice(p,1))}d.length&&(u.innerComments=d)}},c.prototype.findTrailingComments=function(u){var x=[];if(this.trailing.length>0){for(var d=this.trailing.length-1;d>=0;--d){var p=this.trailing[d];p.start>=u.end.offset&&x.unshift(p.comment)}return this.trailing.length=0,x}var h=this.stack[this.stack.length-1];if(h&&h.node.trailingComments){var m=h.node.trailingComments[0];m&&m.range[0]>=u.end.offset&&(x=h.node.trailingComments,delete h.node.trailingComments)}return x},c.prototype.findLeadingComments=function(u){for(var x=[],d;this.stack.length>0;){var p=this.stack[this.stack.length-1];if(p&&p.start>=u.start.offset)d=p.node,this.stack.pop();else break}if(d){for(var h=d.leadingComments?d.leadingComments.length:0,m=h-1;m>=0;--m){var l=d.leadingComments[m];l.range[1]<=u.start.offset&&(x.unshift(l),d.leadingComments.splice(m,1))}return d.leadingComments&&d.leadingComments.length===0&&delete d.leadingComments,x}for(var m=this.leading.length-1;m>=0;--m){var p=this.leading[m];p.start<=u.start.offset&&(x.unshift(p.comment),this.leading.splice(m,1))}return x},c.prototype.visitNode=function(u,x){if(!(u.type===i.Syntax.Program&&u.body.length>0)){this.insertInnerComments(u,x);var d=this.findTrailingComments(x),p=this.findLeadingComments(x);p.length>0&&(u.leadingComments=p),d.length>0&&(u.trailingComments=d),this.stack.push({node:u,start:x.start.offset})}},c.prototype.visitComment=function(u,x){var d=u.type[0]==="L"?"Line":"Block",p={type:d,value:u.value};if(u.range&&(p.range=u.range),u.loc&&(p.loc=u.loc),this.comments.push(p),this.attach){var h={comment:{type:d,value:u.value,range:[x.start.offset,x.end.offset]},start:x.start.offset};u.loc&&(h.comment.loc=u.loc),u.type=d,this.leading.push(h),this.trailing.push(h)}},c.prototype.visit=function(u,x){u.type==="LineComment"?this.visitComment(u,x):u.type==="BlockComment"?this.visitComment(u,x):this.attach&&this.visitNode(u,x)},c}();e.CommentHandler=a},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Syntax={AssignmentExpression:"AssignmentExpression",AssignmentPattern:"AssignmentPattern",ArrayExpression:"ArrayExpression",ArrayPattern:"ArrayPattern",ArrowFunctionExpression:"ArrowFunctionExpression",AwaitExpression:"AwaitExpression",BlockStatement:"BlockStatement",BinaryExpression:"BinaryExpression",BreakStatement:"BreakStatement",CallExpression:"CallExpression",CatchClause:"CatchClause",ClassBody:"ClassBody",ClassDeclaration:"ClassDeclaration",ClassExpression:"ClassExpression",ConditionalExpression:"ConditionalExpression",ContinueStatement:"ContinueStatement",DoWhileStatement:"DoWhileStatement",DebuggerStatement:"DebuggerStatement",EmptyStatement:"EmptyStatement",ExportAllDeclaration:"ExportAllDeclaration",ExportDefaultDeclaration:"ExportDefaultDeclaration",ExportNamedDeclaration:"ExportNamedDeclaration",ExportSpecifier:"ExportSpecifier",ExpressionStatement:"ExpressionStatement",ForStatement:"ForStatement",ForOfStatement:"ForOfStatement",ForInStatement:"ForInStatement",FunctionDeclaration:"FunctionDeclaration",FunctionExpression:"FunctionExpression",Identifier:"Identifier",IfStatement:"IfStatement",ImportDeclaration:"ImportDeclaration",ImportDefaultSpecifier:"ImportDefaultSpecifier",ImportNamespaceSpecifier:"ImportNamespaceSpecifier",ImportSpecifier:"ImportSpecifier",Literal:"Literal",LabeledStatement:"LabeledStatement",LogicalExpression:"LogicalExpression",MemberExpression:"MemberExpression",MetaProperty:"MetaProperty",MethodDefinition:"MethodDefinition",NewExpression:"NewExpression",ObjectExpression:"ObjectExpression",ObjectPattern:"ObjectPattern",Program:"Program",Property:"Property",RestElement:"RestElement",ReturnStatement:"ReturnStatement",SequenceExpression:"SequenceExpression",SpreadElement:"SpreadElement",Super:"Super",SwitchCase:"SwitchCase",SwitchStatement:"SwitchStatement",TaggedTemplateExpression:"TaggedTemplateExpression",TemplateElement:"TemplateElement",TemplateLiteral:"TemplateLiteral",ThisExpression:"ThisExpression",ThrowStatement:"ThrowStatement",TryStatement:"TryStatement",UnaryExpression:"UnaryExpression",UpdateExpression:"UpdateExpression",VariableDeclaration:"VariableDeclaration",VariableDeclarator:"VariableDeclarator",WhileStatement:"WhileStatement",WithStatement:"WithStatement",YieldExpression:"YieldExpression"}},function(t,e,r){"use strict";var i=this&&this.__extends||function(){var n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(s,o){s.__proto__=o}||function(s,o){for(var f in o)o.hasOwnProperty(f)&&(s[f]=o[f])};return function(s,o){n(s,o);function f(){this.constructor=s}s.prototype=o===null?Object.create(o):(f.prototype=o.prototype,new f)}}();Object.defineProperty(e,"__esModule",{value:!0});var a=r(4),c=r(5),u=r(6),x=r(7),d=r(8),p=r(13),h=r(14);p.TokenName[100]="JSXIdentifier",p.TokenName[101]="JSXText";function m(n){var s;switch(n.type){case u.JSXSyntax.JSXIdentifier:var o=n;s=o.name;break;case u.JSXSyntax.JSXNamespacedName:var f=n;s=m(f.namespace)+":"+m(f.name);break;case u.JSXSyntax.JSXMemberExpression:var E=n;s=m(E.object)+"."+m(E.property);break;default:break}return s}var l=function(n){i(s,n);function s(o,f,E){return n.call(this,o,f,E)||this}return s.prototype.parsePrimaryExpression=function(){return this.match("<")?this.parseJSXRoot():n.prototype.parsePrimaryExpression.call(this)},s.prototype.startJSX=function(){this.scanner.index=this.startMarker.index,this.scanner.lineNumber=this.startMarker.line,this.scanner.lineStart=this.startMarker.index-this.startMarker.column},s.prototype.finishJSX=function(){this.nextToken()},s.prototype.reenterJSX=function(){this.startJSX(),this.expectJSX("}"),this.config.tokens&&this.tokens.pop()},s.prototype.createJSXNode=function(){return this.collectComments(),{index:this.scanner.index,line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart}},s.prototype.createJSXChildNode=function(){return{index:this.scanner.index,line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart}},s.prototype.scanXHTMLEntity=function(o){for(var f="&",E=!0,g=!1,v=!1,F=!1;!this.scanner.eof()&&E&&!g;){var A=this.scanner.source[this.scanner.index];if(A===o)break;if(g=A===";",f+=A,++this.scanner.index,!g)switch(f.length){case 2:v=A==="#";break;case 3:v&&(F=A==="x",E=F||a.Character.isDecimalDigit(A.charCodeAt(0)),v=v&&!F);break;default:E=E&&!(v&&!a.Character.isDecimalDigit(A.charCodeAt(0))),E=E&&!(F&&!a.Character.isHexDigit(A.charCodeAt(0)));break}}if(E&&g&&f.length>2){var T=f.substr(1,f.length-2);v&&T.length>1?f=String.fromCharCode(parseInt(T.substr(1),10)):F&&T.length>2?f=String.fromCharCode(parseInt("0"+T.substr(1),16)):!v&&!F&&h.XHTMLEntities[T]&&(f=h.XHTMLEntities[T])}return f},s.prototype.lexJSX=function(){var o=this.scanner.source.charCodeAt(this.scanner.index);if(o===60||o===62||o===47||o===58||o===61||o===123||o===125){var f=this.scanner.source[this.scanner.index++];return{type:7,value:f,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:this.scanner.index-1,end:this.scanner.index}}if(o===34||o===39){for(var E=this.scanner.index,g=this.scanner.source[this.scanner.index++],v="";!this.scanner.eof();){var F=this.scanner.source[this.scanner.index++];if(F===g)break;F==="&"?v+=this.scanXHTMLEntity(g):v+=F}return{type:8,value:v,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:E,end:this.scanner.index}}if(o===46){var A=this.scanner.source.charCodeAt(this.scanner.index+1),T=this.scanner.source.charCodeAt(this.scanner.index+2),f=A===46&&T===46?"...":".",E=this.scanner.index;return this.scanner.index+=f.length,{type:7,value:f,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:E,end:this.scanner.index}}if(o===96)return{type:10,value:"",lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:this.scanner.index,end:this.scanner.index};if(a.Character.isIdentifierStart(o)&&o!==92){var E=this.scanner.index;for(++this.scanner.index;!this.scanner.eof();){var F=this.scanner.source.charCodeAt(this.scanner.index);if(a.Character.isIdentifierPart(F)&&F!==92)++this.scanner.index;else if(F===45)++this.scanner.index;else break}var k=this.scanner.source.slice(E,this.scanner.index);return{type:100,value:k,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:E,end:this.scanner.index}}return this.scanner.lex()},s.prototype.nextJSXToken=function(){this.collectComments(),this.startMarker.index=this.scanner.index,this.startMarker.line=this.scanner.lineNumber,this.startMarker.column=this.scanner.index-this.scanner.lineStart;var o=this.lexJSX();return this.lastMarker.index=this.scanner.index,this.lastMarker.line=this.scanner.lineNumber,this.lastMarker.column=this.scanner.index-this.scanner.lineStart,this.config.tokens&&this.tokens.push(this.convertToken(o)),o},s.prototype.nextJSXText=function(){this.startMarker.index=this.scanner.index,this.startMarker.line=this.scanner.lineNumber,this.startMarker.column=this.scanner.index-this.scanner.lineStart;for(var o=this.scanner.index,f="";!this.scanner.eof();){var E=this.scanner.source[this.scanner.index];if(E==="{"||E==="<")break;++this.scanner.index,f+=E,a.Character.isLineTerminator(E.charCodeAt(0))&&(++this.scanner.lineNumber,E==="\r"&&this.scanner.source[this.scanner.index]===` +`&&++this.scanner.index,this.scanner.lineStart=this.scanner.index)}this.lastMarker.index=this.scanner.index,this.lastMarker.line=this.scanner.lineNumber,this.lastMarker.column=this.scanner.index-this.scanner.lineStart;var g={type:101,value:f,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:o,end:this.scanner.index};return f.length>0&&this.config.tokens&&this.tokens.push(this.convertToken(g)),g},s.prototype.peekJSXToken=function(){var o=this.scanner.saveState();this.scanner.scanComments();var f=this.lexJSX();return this.scanner.restoreState(o),f},s.prototype.expectJSX=function(o){var f=this.nextJSXToken();(f.type!==7||f.value!==o)&&this.throwUnexpectedToken(f)},s.prototype.matchJSX=function(o){var f=this.peekJSXToken();return f.type===7&&f.value===o},s.prototype.parseJSXIdentifier=function(){var o=this.createJSXNode(),f=this.nextJSXToken();return f.type!==100&&this.throwUnexpectedToken(f),this.finalize(o,new c.JSXIdentifier(f.value))},s.prototype.parseJSXElementName=function(){var o=this.createJSXNode(),f=this.parseJSXIdentifier();if(this.matchJSX(":")){var E=f;this.expectJSX(":");var g=this.parseJSXIdentifier();f=this.finalize(o,new c.JSXNamespacedName(E,g))}else if(this.matchJSX("."))for(;this.matchJSX(".");){var v=f;this.expectJSX(".");var F=this.parseJSXIdentifier();f=this.finalize(o,new c.JSXMemberExpression(v,F))}return f},s.prototype.parseJSXAttributeName=function(){var o=this.createJSXNode(),f,E=this.parseJSXIdentifier();if(this.matchJSX(":")){var g=E;this.expectJSX(":");var v=this.parseJSXIdentifier();f=this.finalize(o,new c.JSXNamespacedName(g,v))}else f=E;return f},s.prototype.parseJSXStringLiteralAttribute=function(){var o=this.createJSXNode(),f=this.nextJSXToken();f.type!==8&&this.throwUnexpectedToken(f);var E=this.getTokenRaw(f);return this.finalize(o,new x.Literal(f.value,E))},s.prototype.parseJSXExpressionAttribute=function(){var o=this.createJSXNode();this.expectJSX("{"),this.finishJSX(),this.match("}")&&this.tolerateError("JSX attributes must only be assigned a non-empty expression");var f=this.parseAssignmentExpression();return this.reenterJSX(),this.finalize(o,new c.JSXExpressionContainer(f))},s.prototype.parseJSXAttributeValue=function(){return this.matchJSX("{")?this.parseJSXExpressionAttribute():this.matchJSX("<")?this.parseJSXElement():this.parseJSXStringLiteralAttribute()},s.prototype.parseJSXNameValueAttribute=function(){var o=this.createJSXNode(),f=this.parseJSXAttributeName(),E=null;return this.matchJSX("=")&&(this.expectJSX("="),E=this.parseJSXAttributeValue()),this.finalize(o,new c.JSXAttribute(f,E))},s.prototype.parseJSXSpreadAttribute=function(){var o=this.createJSXNode();this.expectJSX("{"),this.expectJSX("..."),this.finishJSX();var f=this.parseAssignmentExpression();return this.reenterJSX(),this.finalize(o,new c.JSXSpreadAttribute(f))},s.prototype.parseJSXAttributes=function(){for(var o=[];!this.matchJSX("/")&&!this.matchJSX(">");){var f=this.matchJSX("{")?this.parseJSXSpreadAttribute():this.parseJSXNameValueAttribute();o.push(f)}return o},s.prototype.parseJSXOpeningElement=function(){var o=this.createJSXNode();this.expectJSX("<");var f=this.parseJSXElementName(),E=this.parseJSXAttributes(),g=this.matchJSX("/");return g&&this.expectJSX("/"),this.expectJSX(">"),this.finalize(o,new c.JSXOpeningElement(f,g,E))},s.prototype.parseJSXBoundaryElement=function(){var o=this.createJSXNode();if(this.expectJSX("<"),this.matchJSX("/")){this.expectJSX("/");var f=this.parseJSXElementName();return this.expectJSX(">"),this.finalize(o,new c.JSXClosingElement(f))}var E=this.parseJSXElementName(),g=this.parseJSXAttributes(),v=this.matchJSX("/");return v&&this.expectJSX("/"),this.expectJSX(">"),this.finalize(o,new c.JSXOpeningElement(E,v,g))},s.prototype.parseJSXEmptyExpression=function(){var o=this.createJSXChildNode();return this.collectComments(),this.lastMarker.index=this.scanner.index,this.lastMarker.line=this.scanner.lineNumber,this.lastMarker.column=this.scanner.index-this.scanner.lineStart,this.finalize(o,new c.JSXEmptyExpression)},s.prototype.parseJSXExpressionContainer=function(){var o=this.createJSXNode();this.expectJSX("{");var f;return this.matchJSX("}")?(f=this.parseJSXEmptyExpression(),this.expectJSX("}")):(this.finishJSX(),f=this.parseAssignmentExpression(),this.reenterJSX()),this.finalize(o,new c.JSXExpressionContainer(f))},s.prototype.parseJSXChildren=function(){for(var o=[];!this.scanner.eof();){var f=this.createJSXChildNode(),E=this.nextJSXText();if(E.start0){var F=this.finalize(o.node,new c.JSXElement(o.opening,o.children,o.closing));o=f[f.length-1],o.children.push(F),f.pop()}else break}}return o},s.prototype.parseJSXElement=function(){var o=this.createJSXNode(),f=this.parseJSXOpeningElement(),E=[],g=null;if(!f.selfClosing){var v=this.parseComplexJSXElement({node:o,opening:f,closing:g,children:E});E=v.children,g=v.closing}return this.finalize(o,new c.JSXElement(f,E,g))},s.prototype.parseJSXRoot=function(){this.config.tokens&&this.tokens.pop(),this.startJSX();var o=this.parseJSXElement();return this.finishJSX(),o},s.prototype.isStartOfExpression=function(){return n.prototype.isStartOfExpression.call(this)||this.match("<")},s}(d.Parser);e.JSXParser=l},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r={NonAsciiIdentifierStart:/[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309B-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]/,NonAsciiIdentifierPart:/[\xAA\xB5\xB7\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B4\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1369-\u1371\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDEC0-\uDEF8]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/};e.Character={fromCodePoint:function(i){return i<65536?String.fromCharCode(i):String.fromCharCode(55296+(i-65536>>10))+String.fromCharCode(56320+(i-65536&1023))},isWhiteSpace:function(i){return i===32||i===9||i===11||i===12||i===160||i>=5760&&[5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8239,8287,12288,65279].indexOf(i)>=0},isLineTerminator:function(i){return i===10||i===13||i===8232||i===8233},isIdentifierStart:function(i){return i===36||i===95||i>=65&&i<=90||i>=97&&i<=122||i===92||i>=128&&r.NonAsciiIdentifierStart.test(e.Character.fromCodePoint(i))},isIdentifierPart:function(i){return i===36||i===95||i>=65&&i<=90||i>=97&&i<=122||i>=48&&i<=57||i===92||i>=128&&r.NonAsciiIdentifierPart.test(e.Character.fromCodePoint(i))},isDecimalDigit:function(i){return i>=48&&i<=57},isHexDigit:function(i){return i>=48&&i<=57||i>=65&&i<=70||i>=97&&i<=102},isOctalDigit:function(i){return i>=48&&i<=55}}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(6),a=function(){function o(f){this.type=i.JSXSyntax.JSXClosingElement,this.name=f}return o}();e.JSXClosingElement=a;var c=function(){function o(f,E,g){this.type=i.JSXSyntax.JSXElement,this.openingElement=f,this.children=E,this.closingElement=g}return o}();e.JSXElement=c;var u=function(){function o(){this.type=i.JSXSyntax.JSXEmptyExpression}return o}();e.JSXEmptyExpression=u;var x=function(){function o(f){this.type=i.JSXSyntax.JSXExpressionContainer,this.expression=f}return o}();e.JSXExpressionContainer=x;var d=function(){function o(f){this.type=i.JSXSyntax.JSXIdentifier,this.name=f}return o}();e.JSXIdentifier=d;var p=function(){function o(f,E){this.type=i.JSXSyntax.JSXMemberExpression,this.object=f,this.property=E}return o}();e.JSXMemberExpression=p;var h=function(){function o(f,E){this.type=i.JSXSyntax.JSXAttribute,this.name=f,this.value=E}return o}();e.JSXAttribute=h;var m=function(){function o(f,E){this.type=i.JSXSyntax.JSXNamespacedName,this.namespace=f,this.name=E}return o}();e.JSXNamespacedName=m;var l=function(){function o(f,E,g){this.type=i.JSXSyntax.JSXOpeningElement,this.name=f,this.selfClosing=E,this.attributes=g}return o}();e.JSXOpeningElement=l;var n=function(){function o(f){this.type=i.JSXSyntax.JSXSpreadAttribute,this.argument=f}return o}();e.JSXSpreadAttribute=n;var s=function(){function o(f,E){this.type=i.JSXSyntax.JSXText,this.value=f,this.raw=E}return o}();e.JSXText=s},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.JSXSyntax={JSXAttribute:"JSXAttribute",JSXClosingElement:"JSXClosingElement",JSXElement:"JSXElement",JSXEmptyExpression:"JSXEmptyExpression",JSXExpressionContainer:"JSXExpressionContainer",JSXIdentifier:"JSXIdentifier",JSXMemberExpression:"JSXMemberExpression",JSXNamespacedName:"JSXNamespacedName",JSXOpeningElement:"JSXOpeningElement",JSXSpreadAttribute:"JSXSpreadAttribute",JSXText:"JSXText"}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(2),a=function(){function D(y){this.type=i.Syntax.ArrayExpression,this.elements=y}return D}();e.ArrayExpression=a;var c=function(){function D(y){this.type=i.Syntax.ArrayPattern,this.elements=y}return D}();e.ArrayPattern=c;var u=function(){function D(y,w,M){this.type=i.Syntax.ArrowFunctionExpression,this.id=null,this.params=y,this.body=w,this.generator=!1,this.expression=M,this.async=!1}return D}();e.ArrowFunctionExpression=u;var x=function(){function D(y,w,M){this.type=i.Syntax.AssignmentExpression,this.operator=y,this.left=w,this.right=M}return D}();e.AssignmentExpression=x;var d=function(){function D(y,w){this.type=i.Syntax.AssignmentPattern,this.left=y,this.right=w}return D}();e.AssignmentPattern=d;var p=function(){function D(y,w,M){this.type=i.Syntax.ArrowFunctionExpression,this.id=null,this.params=y,this.body=w,this.generator=!1,this.expression=M,this.async=!0}return D}();e.AsyncArrowFunctionExpression=p;var h=function(){function D(y,w,M){this.type=i.Syntax.FunctionDeclaration,this.id=y,this.params=w,this.body=M,this.generator=!1,this.expression=!1,this.async=!0}return D}();e.AsyncFunctionDeclaration=h;var m=function(){function D(y,w,M){this.type=i.Syntax.FunctionExpression,this.id=y,this.params=w,this.body=M,this.generator=!1,this.expression=!1,this.async=!0}return D}();e.AsyncFunctionExpression=m;var l=function(){function D(y){this.type=i.Syntax.AwaitExpression,this.argument=y}return D}();e.AwaitExpression=l;var n=function(){function D(y,w,M){var ae=y==="||"||y==="&&";this.type=ae?i.Syntax.LogicalExpression:i.Syntax.BinaryExpression,this.operator=y,this.left=w,this.right=M}return D}();e.BinaryExpression=n;var s=function(){function D(y){this.type=i.Syntax.BlockStatement,this.body=y}return D}();e.BlockStatement=s;var o=function(){function D(y){this.type=i.Syntax.BreakStatement,this.label=y}return D}();e.BreakStatement=o;var f=function(){function D(y,w){this.type=i.Syntax.CallExpression,this.callee=y,this.arguments=w}return D}();e.CallExpression=f;var E=function(){function D(y,w){this.type=i.Syntax.CatchClause,this.param=y,this.body=w}return D}();e.CatchClause=E;var g=function(){function D(y){this.type=i.Syntax.ClassBody,this.body=y}return D}();e.ClassBody=g;var v=function(){function D(y,w,M){this.type=i.Syntax.ClassDeclaration,this.id=y,this.superClass=w,this.body=M}return D}();e.ClassDeclaration=v;var F=function(){function D(y,w,M){this.type=i.Syntax.ClassExpression,this.id=y,this.superClass=w,this.body=M}return D}();e.ClassExpression=F;var A=function(){function D(y,w){this.type=i.Syntax.MemberExpression,this.computed=!0,this.object=y,this.property=w}return D}();e.ComputedMemberExpression=A;var T=function(){function D(y,w,M){this.type=i.Syntax.ConditionalExpression,this.test=y,this.consequent=w,this.alternate=M}return D}();e.ConditionalExpression=T;var k=function(){function D(y){this.type=i.Syntax.ContinueStatement,this.label=y}return D}();e.ContinueStatement=k;var P=function(){function D(){this.type=i.Syntax.DebuggerStatement}return D}();e.DebuggerStatement=P;var N=function(){function D(y,w){this.type=i.Syntax.ExpressionStatement,this.expression=y,this.directive=w}return D}();e.Directive=N;var U=function(){function D(y,w){this.type=i.Syntax.DoWhileStatement,this.body=y,this.test=w}return D}();e.DoWhileStatement=U;var S=function(){function D(){this.type=i.Syntax.EmptyStatement}return D}();e.EmptyStatement=S;var j=function(){function D(y){this.type=i.Syntax.ExportAllDeclaration,this.source=y}return D}();e.ExportAllDeclaration=j;var J=function(){function D(y){this.type=i.Syntax.ExportDefaultDeclaration,this.declaration=y}return D}();e.ExportDefaultDeclaration=J;var ye=function(){function D(y,w,M){this.type=i.Syntax.ExportNamedDeclaration,this.declaration=y,this.specifiers=w,this.source=M}return D}();e.ExportNamedDeclaration=ye;var C=function(){function D(y,w){this.type=i.Syntax.ExportSpecifier,this.exported=w,this.local=y}return D}();e.ExportSpecifier=C;var b=function(){function D(y){this.type=i.Syntax.ExpressionStatement,this.expression=y}return D}();e.ExpressionStatement=b;var Y=function(){function D(y,w,M){this.type=i.Syntax.ForInStatement,this.left=y,this.right=w,this.body=M,this.each=!1}return D}();e.ForInStatement=Y;var $=function(){function D(y,w,M){this.type=i.Syntax.ForOfStatement,this.left=y,this.right=w,this.body=M}return D}();e.ForOfStatement=$;var he=function(){function D(y,w,M,ae){this.type=i.Syntax.ForStatement,this.init=y,this.test=w,this.update=M,this.body=ae}return D}();e.ForStatement=he;var fe=function(){function D(y,w,M,ae){this.type=i.Syntax.FunctionDeclaration,this.id=y,this.params=w,this.body=M,this.generator=ae,this.expression=!1,this.async=!1}return D}();e.FunctionDeclaration=fe;var ie=function(){function D(y,w,M,ae){this.type=i.Syntax.FunctionExpression,this.id=y,this.params=w,this.body=M,this.generator=ae,this.expression=!1,this.async=!1}return D}();e.FunctionExpression=ie;var Ye=function(){function D(y){this.type=i.Syntax.Identifier,this.name=y}return D}();e.Identifier=Ye;var Ar=function(){function D(y,w,M){this.type=i.Syntax.IfStatement,this.test=y,this.consequent=w,this.alternate=M}return D}();e.IfStatement=Ar;var Qe=function(){function D(y,w){this.type=i.Syntax.ImportDeclaration,this.specifiers=y,this.source=w}return D}();e.ImportDeclaration=Qe;var we=function(){function D(y){this.type=i.Syntax.ImportDefaultSpecifier,this.local=y}return D}();e.ImportDefaultSpecifier=we;var X=function(){function D(y){this.type=i.Syntax.ImportNamespaceSpecifier,this.local=y}return D}();e.ImportNamespaceSpecifier=X;var Ze=function(){function D(y,w){this.type=i.Syntax.ImportSpecifier,this.local=y,this.imported=w}return D}();e.ImportSpecifier=Ze;var Cr=function(){function D(y,w){this.type=i.Syntax.LabeledStatement,this.label=y,this.body=w}return D}();e.LabeledStatement=Cr;var R=function(){function D(y,w){this.type=i.Syntax.Literal,this.value=y,this.raw=w}return D}();e.Literal=R;var z=function(){function D(y,w){this.type=i.Syntax.MetaProperty,this.meta=y,this.property=w}return D}();e.MetaProperty=z;var B=function(){function D(y,w,M,ae,br){this.type=i.Syntax.MethodDefinition,this.key=y,this.computed=w,this.value=M,this.kind=ae,this.static=br}return D}();e.MethodDefinition=B;var H=function(){function D(y){this.type=i.Syntax.Program,this.body=y,this.sourceType="module"}return D}();e.Module=H;var q=function(){function D(y,w){this.type=i.Syntax.NewExpression,this.callee=y,this.arguments=w}return D}();e.NewExpression=q;var Q=function(){function D(y){this.type=i.Syntax.ObjectExpression,this.properties=y}return D}();e.ObjectExpression=Q;var K=function(){function D(y){this.type=i.Syntax.ObjectPattern,this.properties=y}return D}();e.ObjectPattern=K;var pt=function(){function D(y,w,M,ae,br,yl){this.type=i.Syntax.Property,this.key=w,this.computed=M,this.value=ae,this.kind=y,this.method=br,this.shorthand=yl}return D}();e.Property=pt;var et=function(){function D(y,w,M,ae){this.type=i.Syntax.Literal,this.value=y,this.raw=w,this.regex={pattern:M,flags:ae}}return D}();e.RegexLiteral=et;var Qc=function(){function D(y){this.type=i.Syntax.RestElement,this.argument=y}return D}();e.RestElement=Qc;var Zc=function(){function D(y){this.type=i.Syntax.ReturnStatement,this.argument=y}return D}();e.ReturnStatement=Zc;var el=function(){function D(y){this.type=i.Syntax.Program,this.body=y,this.sourceType="script"}return D}();e.Script=el;var tl=function(){function D(y){this.type=i.Syntax.SequenceExpression,this.expressions=y}return D}();e.SequenceExpression=tl;var rl=function(){function D(y){this.type=i.Syntax.SpreadElement,this.argument=y}return D}();e.SpreadElement=rl;var il=function(){function D(y,w){this.type=i.Syntax.MemberExpression,this.computed=!1,this.object=y,this.property=w}return D}();e.StaticMemberExpression=il;var nl=function(){function D(){this.type=i.Syntax.Super}return D}();e.Super=nl;var sl=function(){function D(y,w){this.type=i.Syntax.SwitchCase,this.test=y,this.consequent=w}return D}();e.SwitchCase=sl;var al=function(){function D(y,w){this.type=i.Syntax.SwitchStatement,this.discriminant=y,this.cases=w}return D}();e.SwitchStatement=al;var ol=function(){function D(y,w){this.type=i.Syntax.TaggedTemplateExpression,this.tag=y,this.quasi=w}return D}();e.TaggedTemplateExpression=ol;var ul=function(){function D(y,w){this.type=i.Syntax.TemplateElement,this.value=y,this.tail=w}return D}();e.TemplateElement=ul;var cl=function(){function D(y,w){this.type=i.Syntax.TemplateLiteral,this.quasis=y,this.expressions=w}return D}();e.TemplateLiteral=cl;var ll=function(){function D(){this.type=i.Syntax.ThisExpression}return D}();e.ThisExpression=ll;var hl=function(){function D(y){this.type=i.Syntax.ThrowStatement,this.argument=y}return D}();e.ThrowStatement=hl;var fl=function(){function D(y,w,M){this.type=i.Syntax.TryStatement,this.block=y,this.handler=w,this.finalizer=M}return D}();e.TryStatement=fl;var pl=function(){function D(y,w){this.type=i.Syntax.UnaryExpression,this.operator=y,this.argument=w,this.prefix=!0}return D}();e.UnaryExpression=pl;var dl=function(){function D(y,w,M){this.type=i.Syntax.UpdateExpression,this.operator=y,this.argument=w,this.prefix=M}return D}();e.UpdateExpression=dl;var ml=function(){function D(y,w){this.type=i.Syntax.VariableDeclaration,this.declarations=y,this.kind=w}return D}();e.VariableDeclaration=ml;var xl=function(){function D(y,w){this.type=i.Syntax.VariableDeclarator,this.id=y,this.init=w}return D}();e.VariableDeclarator=xl;var El=function(){function D(y,w){this.type=i.Syntax.WhileStatement,this.test=y,this.body=w}return D}();e.WhileStatement=El;var gl=function(){function D(y,w){this.type=i.Syntax.WithStatement,this.object=y,this.body=w}return D}();e.WithStatement=gl;var Dl=function(){function D(y,w){this.type=i.Syntax.YieldExpression,this.argument=y,this.delegate=w}return D}();e.YieldExpression=Dl},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(9),a=r(10),c=r(11),u=r(7),x=r(12),d=r(2),p=r(13),h="ArrowParameterPlaceHolder",m=function(){function l(n,s,o){s===void 0&&(s={}),this.config={range:typeof s.range=="boolean"&&s.range,loc:typeof s.loc=="boolean"&&s.loc,source:null,tokens:typeof s.tokens=="boolean"&&s.tokens,comment:typeof s.comment=="boolean"&&s.comment,tolerant:typeof s.tolerant=="boolean"&&s.tolerant},this.config.loc&&s.source&&s.source!==null&&(this.config.source=String(s.source)),this.delegate=o,this.errorHandler=new a.ErrorHandler,this.errorHandler.tolerant=this.config.tolerant,this.scanner=new x.Scanner(n,this.errorHandler),this.scanner.trackComment=this.config.comment,this.operatorPrecedence={")":0,";":0,",":0,"=":0,"]":0,"||":1,"&&":2,"|":3,"^":4,"&":5,"==":6,"!=":6,"===":6,"!==":6,"<":7,">":7,"<=":7,">=":7,"<<":8,">>":8,">>>":8,"+":9,"-":9,"*":11,"/":11,"%":11},this.lookahead={type:2,value:"",lineNumber:this.scanner.lineNumber,lineStart:0,start:0,end:0},this.hasLineTerminator=!1,this.context={isModule:!1,await:!1,allowIn:!0,allowStrictDirective:!0,allowYield:!0,firstCoverInitializedNameError:null,isAssignmentTarget:!1,isBindingElement:!1,inFunctionBody:!1,inIteration:!1,inSwitch:!1,labelSet:{},strict:!1},this.tokens=[],this.startMarker={index:0,line:this.scanner.lineNumber,column:0},this.lastMarker={index:0,line:this.scanner.lineNumber,column:0},this.nextToken(),this.lastMarker={index:this.scanner.index,line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart}}return l.prototype.throwError=function(n){for(var s=[],o=1;o0&&this.delegate)for(var s=0;s>="||n===">>>="||n==="&="||n==="^="||n==="|="},l.prototype.isolateCoverGrammar=function(n){var s=this.context.isBindingElement,o=this.context.isAssignmentTarget,f=this.context.firstCoverInitializedNameError;this.context.isBindingElement=!0,this.context.isAssignmentTarget=!0,this.context.firstCoverInitializedNameError=null;var E=n.call(this);return this.context.firstCoverInitializedNameError!==null&&this.throwUnexpectedToken(this.context.firstCoverInitializedNameError),this.context.isBindingElement=s,this.context.isAssignmentTarget=o,this.context.firstCoverInitializedNameError=f,E},l.prototype.inheritCoverGrammar=function(n){var s=this.context.isBindingElement,o=this.context.isAssignmentTarget,f=this.context.firstCoverInitializedNameError;this.context.isBindingElement=!0,this.context.isAssignmentTarget=!0,this.context.firstCoverInitializedNameError=null;var E=n.call(this);return this.context.isBindingElement=this.context.isBindingElement&&s,this.context.isAssignmentTarget=this.context.isAssignmentTarget&&o,this.context.firstCoverInitializedNameError=f||this.context.firstCoverInitializedNameError,E},l.prototype.consumeSemicolon=function(){this.match(";")?this.nextToken():this.hasLineTerminator||(this.lookahead.type!==2&&!this.match("}")&&this.throwUnexpectedToken(this.lookahead),this.lastMarker.index=this.startMarker.index,this.lastMarker.line=this.startMarker.line,this.lastMarker.column=this.startMarker.column)},l.prototype.parsePrimaryExpression=function(){var n=this.createNode(),s,o,f;switch(this.lookahead.type){case 3:(this.context.isModule||this.context.await)&&this.lookahead.value==="await"&&this.tolerateUnexpectedToken(this.lookahead),s=this.matchAsyncFunction()?this.parseFunctionExpression():this.finalize(n,new u.Identifier(this.nextToken().value));break;case 6:case 8:this.context.strict&&this.lookahead.octal&&this.tolerateUnexpectedToken(this.lookahead,c.Messages.StrictOctalLiteral),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,o=this.nextToken(),f=this.getTokenRaw(o),s=this.finalize(n,new u.Literal(o.value,f));break;case 1:this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,o=this.nextToken(),f=this.getTokenRaw(o),s=this.finalize(n,new u.Literal(o.value==="true",f));break;case 5:this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,o=this.nextToken(),f=this.getTokenRaw(o),s=this.finalize(n,new u.Literal(null,f));break;case 10:s=this.parseTemplateLiteral();break;case 7:switch(this.lookahead.value){case"(":this.context.isBindingElement=!1,s=this.inheritCoverGrammar(this.parseGroupExpression);break;case"[":s=this.inheritCoverGrammar(this.parseArrayInitializer);break;case"{":s=this.inheritCoverGrammar(this.parseObjectInitializer);break;case"/":case"/=":this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,this.scanner.index=this.startMarker.index,o=this.nextRegexToken(),f=this.getTokenRaw(o),s=this.finalize(n,new u.RegexLiteral(o.regex,f,o.pattern,o.flags));break;default:s=this.throwUnexpectedToken(this.nextToken())}break;case 4:!this.context.strict&&this.context.allowYield&&this.matchKeyword("yield")?s=this.parseIdentifierName():!this.context.strict&&this.matchKeyword("let")?s=this.finalize(n,new u.Identifier(this.nextToken().value)):(this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,this.matchKeyword("function")?s=this.parseFunctionExpression():this.matchKeyword("this")?(this.nextToken(),s=this.finalize(n,new u.ThisExpression)):this.matchKeyword("class")?s=this.parseClassExpression():s=this.throwUnexpectedToken(this.nextToken()));break;default:s=this.throwUnexpectedToken(this.nextToken())}return s},l.prototype.parseSpreadElement=function(){var n=this.createNode();this.expect("...");var s=this.inheritCoverGrammar(this.parseAssignmentExpression);return this.finalize(n,new u.SpreadElement(s))},l.prototype.parseArrayInitializer=function(){var n=this.createNode(),s=[];for(this.expect("[");!this.match("]");)if(this.match(","))this.nextToken(),s.push(null);else if(this.match("...")){var o=this.parseSpreadElement();this.match("]")||(this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,this.expect(",")),s.push(o)}else s.push(this.inheritCoverGrammar(this.parseAssignmentExpression)),this.match("]")||this.expect(",");return this.expect("]"),this.finalize(n,new u.ArrayExpression(s))},l.prototype.parsePropertyMethod=function(n){this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1;var s=this.context.strict,o=this.context.allowStrictDirective;this.context.allowStrictDirective=n.simple;var f=this.isolateCoverGrammar(this.parseFunctionSourceElements);return this.context.strict&&n.firstRestricted&&this.tolerateUnexpectedToken(n.firstRestricted,n.message),this.context.strict&&n.stricted&&this.tolerateUnexpectedToken(n.stricted,n.message),this.context.strict=s,this.context.allowStrictDirective=o,f},l.prototype.parsePropertyMethodFunction=function(){var n=!1,s=this.createNode(),o=this.context.allowYield;this.context.allowYield=!0;var f=this.parseFormalParameters(),E=this.parsePropertyMethod(f);return this.context.allowYield=o,this.finalize(s,new u.FunctionExpression(null,f.params,E,n))},l.prototype.parsePropertyMethodAsyncFunction=function(){var n=this.createNode(),s=this.context.allowYield,o=this.context.await;this.context.allowYield=!1,this.context.await=!0;var f=this.parseFormalParameters(),E=this.parsePropertyMethod(f);return this.context.allowYield=s,this.context.await=o,this.finalize(n,new u.AsyncFunctionExpression(null,f.params,E))},l.prototype.parseObjectPropertyKey=function(){var n=this.createNode(),s=this.nextToken(),o;switch(s.type){case 8:case 6:this.context.strict&&s.octal&&this.tolerateUnexpectedToken(s,c.Messages.StrictOctalLiteral);var f=this.getTokenRaw(s);o=this.finalize(n,new u.Literal(s.value,f));break;case 3:case 1:case 5:case 4:o=this.finalize(n,new u.Identifier(s.value));break;case 7:s.value==="["?(o=this.isolateCoverGrammar(this.parseAssignmentExpression),this.expect("]")):o=this.throwUnexpectedToken(s);break;default:o=this.throwUnexpectedToken(s)}return o},l.prototype.isPropertyKey=function(n,s){return n.type===d.Syntax.Identifier&&n.name===s||n.type===d.Syntax.Literal&&n.value===s},l.prototype.parseObjectProperty=function(n){var s=this.createNode(),o=this.lookahead,f,E=null,g=null,v=!1,F=!1,A=!1,T=!1;if(o.type===3){var k=o.value;this.nextToken(),v=this.match("["),T=!this.hasLineTerminator&&k==="async"&&!this.match(":")&&!this.match("(")&&!this.match("*")&&!this.match(","),E=T?this.parseObjectPropertyKey():this.finalize(s,new u.Identifier(k))}else this.match("*")?this.nextToken():(v=this.match("["),E=this.parseObjectPropertyKey());var P=this.qualifiedPropertyName(this.lookahead);if(o.type===3&&!T&&o.value==="get"&&P)f="get",v=this.match("["),E=this.parseObjectPropertyKey(),this.context.allowYield=!1,g=this.parseGetterMethod();else if(o.type===3&&!T&&o.value==="set"&&P)f="set",v=this.match("["),E=this.parseObjectPropertyKey(),g=this.parseSetterMethod();else if(o.type===7&&o.value==="*"&&P)f="init",v=this.match("["),E=this.parseObjectPropertyKey(),g=this.parseGeneratorMethod(),F=!0;else if(E||this.throwUnexpectedToken(this.lookahead),f="init",this.match(":")&&!T)!v&&this.isPropertyKey(E,"__proto__")&&(n.value&&this.tolerateError(c.Messages.DuplicateProtoProperty),n.value=!0),this.nextToken(),g=this.inheritCoverGrammar(this.parseAssignmentExpression);else if(this.match("("))g=T?this.parsePropertyMethodAsyncFunction():this.parsePropertyMethodFunction(),F=!0;else if(o.type===3){var k=this.finalize(s,new u.Identifier(o.value));if(this.match("=")){this.context.firstCoverInitializedNameError=this.lookahead,this.nextToken(),A=!0;var N=this.isolateCoverGrammar(this.parseAssignmentExpression);g=this.finalize(s,new u.AssignmentPattern(k,N))}else A=!0,g=k}else this.throwUnexpectedToken(this.nextToken());return this.finalize(s,new u.Property(f,E,v,g,F,A))},l.prototype.parseObjectInitializer=function(){var n=this.createNode();this.expect("{");for(var s=[],o={value:!1};!this.match("}");)s.push(this.parseObjectProperty(o)),this.match("}")||this.expectCommaSeparator();return this.expect("}"),this.finalize(n,new u.ObjectExpression(s))},l.prototype.parseTemplateHead=function(){i.assert(this.lookahead.head,"Template literal must start with a template head");var n=this.createNode(),s=this.nextToken(),o=s.value,f=s.cooked;return this.finalize(n,new u.TemplateElement({raw:o,cooked:f},s.tail))},l.prototype.parseTemplateElement=function(){this.lookahead.type!==10&&this.throwUnexpectedToken();var n=this.createNode(),s=this.nextToken(),o=s.value,f=s.cooked;return this.finalize(n,new u.TemplateElement({raw:o,cooked:f},s.tail))},l.prototype.parseTemplateLiteral=function(){var n=this.createNode(),s=[],o=[],f=this.parseTemplateHead();for(o.push(f);!f.tail;)s.push(this.parseExpression()),f=this.parseTemplateElement(),o.push(f);return this.finalize(n,new u.TemplateLiteral(o,s))},l.prototype.reinterpretExpressionAsPattern=function(n){switch(n.type){case d.Syntax.Identifier:case d.Syntax.MemberExpression:case d.Syntax.RestElement:case d.Syntax.AssignmentPattern:break;case d.Syntax.SpreadElement:n.type=d.Syntax.RestElement,this.reinterpretExpressionAsPattern(n.argument);break;case d.Syntax.ArrayExpression:n.type=d.Syntax.ArrayPattern;for(var s=0;s")||this.expect("=>"),n={type:h,params:[],async:!1};else{var s=this.lookahead,o=[];if(this.match("..."))n=this.parseRestElement(o),this.expect(")"),this.match("=>")||this.expect("=>"),n={type:h,params:[n],async:!1};else{var f=!1;if(this.context.isBindingElement=!0,n=this.inheritCoverGrammar(this.parseAssignmentExpression),this.match(",")){var E=[];for(this.context.isAssignmentTarget=!1,E.push(n);this.lookahead.type!==2&&this.match(",");){if(this.nextToken(),this.match(")")){this.nextToken();for(var g=0;g")||this.expect("=>"),this.context.isBindingElement=!1;for(var g=0;g")&&(n.type===d.Syntax.Identifier&&n.name==="yield"&&(f=!0,n={type:h,params:[n],async:!1}),!f)){if(this.context.isBindingElement||this.throwUnexpectedToken(this.lookahead),n.type===d.Syntax.SequenceExpression)for(var g=0;g")){for(var F=0;F0){this.nextToken(),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1;for(var E=[n,this.lookahead],g=s,v=this.isolateCoverGrammar(this.parseExponentiationExpression),F=[g,o.value,v],A=[f];f=this.binaryPrecedence(this.lookahead),!(f<=0);){for(;F.length>2&&f<=A[A.length-1];){v=F.pop();var T=F.pop();A.pop(),g=F.pop(),E.pop();var k=this.startNode(E[E.length-1]);F.push(this.finalize(k,new u.BinaryExpression(T,g,v)))}F.push(this.nextToken().value),A.push(f),E.push(this.lookahead),F.push(this.isolateCoverGrammar(this.parseExponentiationExpression))}var P=F.length-1;s=F[P];for(var N=E.pop();P>1;){var U=E.pop(),S=N&&N.lineStart,k=this.startNode(U,S),T=F[P-1];s=this.finalize(k,new u.BinaryExpression(T,F[P-2],s)),P-=2,N=U}}return s},l.prototype.parseConditionalExpression=function(){var n=this.lookahead,s=this.inheritCoverGrammar(this.parseBinaryExpression);if(this.match("?")){this.nextToken();var o=this.context.allowIn;this.context.allowIn=!0;var f=this.isolateCoverGrammar(this.parseAssignmentExpression);this.context.allowIn=o,this.expect(":");var E=this.isolateCoverGrammar(this.parseAssignmentExpression);s=this.finalize(this.startNode(n),new u.ConditionalExpression(s,f,E)),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1}return s},l.prototype.checkPatternParam=function(n,s){switch(s.type){case d.Syntax.Identifier:this.validateParam(n,s,s.name);break;case d.Syntax.RestElement:this.checkPatternParam(n,s.argument);break;case d.Syntax.AssignmentPattern:this.checkPatternParam(n,s.left);break;case d.Syntax.ArrayPattern:for(var o=0;o")){this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1;var E=n.async,g=this.reinterpretAsCoverFormalsList(n);if(g){this.hasLineTerminator&&this.tolerateUnexpectedToken(this.lookahead),this.context.firstCoverInitializedNameError=null;var v=this.context.strict,F=this.context.allowStrictDirective;this.context.allowStrictDirective=g.simple;var A=this.context.allowYield,T=this.context.await;this.context.allowYield=!0,this.context.await=E;var k=this.startNode(s);this.expect("=>");var P=void 0;if(this.match("{")){var N=this.context.allowIn;this.context.allowIn=!0,P=this.parseFunctionSourceElements(),this.context.allowIn=N}else P=this.isolateCoverGrammar(this.parseAssignmentExpression);var U=P.type!==d.Syntax.BlockStatement;this.context.strict&&g.firstRestricted&&this.throwUnexpectedToken(g.firstRestricted,g.message),this.context.strict&&g.stricted&&this.tolerateUnexpectedToken(g.stricted,g.message),n=E?this.finalize(k,new u.AsyncArrowFunctionExpression(g.params,P,U)):this.finalize(k,new u.ArrowFunctionExpression(g.params,P,U)),this.context.strict=v,this.context.allowStrictDirective=F,this.context.allowYield=A,this.context.await=T}}else if(this.matchAssign()){if(this.context.isAssignmentTarget||this.tolerateError(c.Messages.InvalidLHSInAssignment),this.context.strict&&n.type===d.Syntax.Identifier){var S=n;this.scanner.isRestrictedWord(S.name)&&this.tolerateUnexpectedToken(o,c.Messages.StrictLHSAssignment),this.scanner.isStrictModeReservedWord(S.name)&&this.tolerateUnexpectedToken(o,c.Messages.StrictReservedWord)}this.match("=")?this.reinterpretExpressionAsPattern(n):(this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1),o=this.nextToken();var j=o.value,J=this.isolateCoverGrammar(this.parseAssignmentExpression);n=this.finalize(this.startNode(s),new u.AssignmentExpression(j,n,J)),this.context.firstCoverInitializedNameError=null}}return n},l.prototype.parseExpression=function(){var n=this.lookahead,s=this.isolateCoverGrammar(this.parseAssignmentExpression);if(this.match(",")){var o=[];for(o.push(s);this.lookahead.type!==2&&this.match(",");)this.nextToken(),o.push(this.isolateCoverGrammar(this.parseAssignmentExpression));s=this.finalize(this.startNode(n),new u.SequenceExpression(o))}return s},l.prototype.parseStatementListItem=function(){var n;if(this.context.isAssignmentTarget=!0,this.context.isBindingElement=!0,this.lookahead.type===4)switch(this.lookahead.value){case"export":this.context.isModule||this.tolerateUnexpectedToken(this.lookahead,c.Messages.IllegalExportDeclaration),n=this.parseExportDeclaration();break;case"import":this.context.isModule||this.tolerateUnexpectedToken(this.lookahead,c.Messages.IllegalImportDeclaration),n=this.parseImportDeclaration();break;case"const":n=this.parseLexicalDeclaration({inFor:!1});break;case"function":n=this.parseFunctionDeclaration();break;case"class":n=this.parseClassDeclaration();break;case"let":n=this.isLexicalDeclaration()?this.parseLexicalDeclaration({inFor:!1}):this.parseStatement();break;default:n=this.parseStatement();break}else n=this.parseStatement();return n},l.prototype.parseBlock=function(){var n=this.createNode();this.expect("{");for(var s=[];!this.match("}");)s.push(this.parseStatementListItem());return this.expect("}"),this.finalize(n,new u.BlockStatement(s))},l.prototype.parseLexicalBinding=function(n,s){var o=this.createNode(),f=[],E=this.parsePattern(f,n);this.context.strict&&E.type===d.Syntax.Identifier&&this.scanner.isRestrictedWord(E.name)&&this.tolerateError(c.Messages.StrictVarName);var g=null;return n==="const"?!this.matchKeyword("in")&&!this.matchContextualKeyword("of")&&(this.match("=")?(this.nextToken(),g=this.isolateCoverGrammar(this.parseAssignmentExpression)):this.throwError(c.Messages.DeclarationMissingInitializer,"const")):(!s.inFor&&E.type!==d.Syntax.Identifier||this.match("="))&&(this.expect("="),g=this.isolateCoverGrammar(this.parseAssignmentExpression)),this.finalize(o,new u.VariableDeclarator(E,g))},l.prototype.parseBindingList=function(n,s){for(var o=[this.parseLexicalBinding(n,s)];this.match(",");)this.nextToken(),o.push(this.parseLexicalBinding(n,s));return o},l.prototype.isLexicalDeclaration=function(){var n=this.scanner.saveState();this.scanner.scanComments();var s=this.scanner.lex();return this.scanner.restoreState(n),s.type===3||s.type===7&&s.value==="["||s.type===7&&s.value==="{"||s.type===4&&s.value==="let"||s.type===4&&s.value==="yield"},l.prototype.parseLexicalDeclaration=function(n){var s=this.createNode(),o=this.nextToken().value;i.assert(o==="let"||o==="const","Lexical declaration must be either let or const");var f=this.parseBindingList(o,n);return this.consumeSemicolon(),this.finalize(s,new u.VariableDeclaration(f,o))},l.prototype.parseBindingRestElement=function(n,s){var o=this.createNode();this.expect("...");var f=this.parsePattern(n,s);return this.finalize(o,new u.RestElement(f))},l.prototype.parseArrayPattern=function(n,s){var o=this.createNode();this.expect("[");for(var f=[];!this.match("]");)if(this.match(","))this.nextToken(),f.push(null);else{if(this.match("...")){f.push(this.parseBindingRestElement(n,s));break}else f.push(this.parsePatternWithDefault(n,s));this.match("]")||this.expect(",")}return this.expect("]"),this.finalize(o,new u.ArrayPattern(f))},l.prototype.parsePropertyPattern=function(n,s){var o=this.createNode(),f=!1,E=!1,g=!1,v,F;if(this.lookahead.type===3){var A=this.lookahead;v=this.parseVariableIdentifier();var T=this.finalize(o,new u.Identifier(A.value));if(this.match("=")){n.push(A),E=!0,this.nextToken();var k=this.parseAssignmentExpression();F=this.finalize(this.startNode(A),new u.AssignmentPattern(T,k))}else this.match(":")?(this.expect(":"),F=this.parsePatternWithDefault(n,s)):(n.push(A),E=!0,F=T)}else f=this.match("["),v=this.parseObjectPropertyKey(),this.expect(":"),F=this.parsePatternWithDefault(n,s);return this.finalize(o,new u.Property("init",v,f,F,g,E))},l.prototype.parseObjectPattern=function(n,s){var o=this.createNode(),f=[];for(this.expect("{");!this.match("}");)f.push(this.parsePropertyPattern(n,s)),this.match("}")||this.expect(",");return this.expect("}"),this.finalize(o,new u.ObjectPattern(f))},l.prototype.parsePattern=function(n,s){var o;return this.match("[")?o=this.parseArrayPattern(n,s):this.match("{")?o=this.parseObjectPattern(n,s):(this.matchKeyword("let")&&(s==="const"||s==="let")&&this.tolerateUnexpectedToken(this.lookahead,c.Messages.LetInLexicalBinding),n.push(this.lookahead),o=this.parseVariableIdentifier(s)),o},l.prototype.parsePatternWithDefault=function(n,s){var o=this.lookahead,f=this.parsePattern(n,s);if(this.match("=")){this.nextToken();var E=this.context.allowYield;this.context.allowYield=!0;var g=this.isolateCoverGrammar(this.parseAssignmentExpression);this.context.allowYield=E,f=this.finalize(this.startNode(o),new u.AssignmentPattern(f,g))}return f},l.prototype.parseVariableIdentifier=function(n){var s=this.createNode(),o=this.nextToken();return o.type===4&&o.value==="yield"?this.context.strict?this.tolerateUnexpectedToken(o,c.Messages.StrictReservedWord):this.context.allowYield||this.throwUnexpectedToken(o):o.type!==3?this.context.strict&&o.type===4&&this.scanner.isStrictModeReservedWord(o.value)?this.tolerateUnexpectedToken(o,c.Messages.StrictReservedWord):(this.context.strict||o.value!=="let"||n!=="var")&&this.throwUnexpectedToken(o):(this.context.isModule||this.context.await)&&o.type===3&&o.value==="await"&&this.tolerateUnexpectedToken(o),this.finalize(s,new u.Identifier(o.value))},l.prototype.parseVariableDeclaration=function(n){var s=this.createNode(),o=[],f=this.parsePattern(o,"var");this.context.strict&&f.type===d.Syntax.Identifier&&this.scanner.isRestrictedWord(f.name)&&this.tolerateError(c.Messages.StrictVarName);var E=null;return this.match("=")?(this.nextToken(),E=this.isolateCoverGrammar(this.parseAssignmentExpression)):f.type!==d.Syntax.Identifier&&!n.inFor&&this.expect("="),this.finalize(s,new u.VariableDeclarator(f,E))},l.prototype.parseVariableDeclarationList=function(n){var s={inFor:n.inFor},o=[];for(o.push(this.parseVariableDeclaration(s));this.match(",");)this.nextToken(),o.push(this.parseVariableDeclaration(s));return o},l.prototype.parseVariableStatement=function(){var n=this.createNode();this.expectKeyword("var");var s=this.parseVariableDeclarationList({inFor:!1});return this.consumeSemicolon(),this.finalize(n,new u.VariableDeclaration(s,"var"))},l.prototype.parseEmptyStatement=function(){var n=this.createNode();return this.expect(";"),this.finalize(n,new u.EmptyStatement)},l.prototype.parseExpressionStatement=function(){var n=this.createNode(),s=this.parseExpression();return this.consumeSemicolon(),this.finalize(n,new u.ExpressionStatement(s))},l.prototype.parseIfClause=function(){return this.context.strict&&this.matchKeyword("function")&&this.tolerateError(c.Messages.StrictFunction),this.parseStatement()},l.prototype.parseIfStatement=function(){var n=this.createNode(),s,o=null;this.expectKeyword("if"),this.expect("(");var f=this.parseExpression();return!this.match(")")&&this.config.tolerant?(this.tolerateUnexpectedToken(this.nextToken()),s=this.finalize(this.createNode(),new u.EmptyStatement)):(this.expect(")"),s=this.parseIfClause(),this.matchKeyword("else")&&(this.nextToken(),o=this.parseIfClause())),this.finalize(n,new u.IfStatement(f,s,o))},l.prototype.parseDoWhileStatement=function(){var n=this.createNode();this.expectKeyword("do");var s=this.context.inIteration;this.context.inIteration=!0;var o=this.parseStatement();this.context.inIteration=s,this.expectKeyword("while"),this.expect("(");var f=this.parseExpression();return!this.match(")")&&this.config.tolerant?this.tolerateUnexpectedToken(this.nextToken()):(this.expect(")"),this.match(";")&&this.nextToken()),this.finalize(n,new u.DoWhileStatement(o,f))},l.prototype.parseWhileStatement=function(){var n=this.createNode(),s;this.expectKeyword("while"),this.expect("(");var o=this.parseExpression();if(!this.match(")")&&this.config.tolerant)this.tolerateUnexpectedToken(this.nextToken()),s=this.finalize(this.createNode(),new u.EmptyStatement);else{this.expect(")");var f=this.context.inIteration;this.context.inIteration=!0,s=this.parseStatement(),this.context.inIteration=f}return this.finalize(n,new u.WhileStatement(o,s))},l.prototype.parseForStatement=function(){var n=null,s=null,o=null,f=!0,E,g,v=this.createNode();if(this.expectKeyword("for"),this.expect("("),this.match(";"))this.nextToken();else if(this.matchKeyword("var")){n=this.createNode(),this.nextToken();var F=this.context.allowIn;this.context.allowIn=!1;var A=this.parseVariableDeclarationList({inFor:!0});if(this.context.allowIn=F,A.length===1&&this.matchKeyword("in")){var T=A[0];T.init&&(T.id.type===d.Syntax.ArrayPattern||T.id.type===d.Syntax.ObjectPattern||this.context.strict)&&this.tolerateError(c.Messages.ForInOfLoopInitializer,"for-in"),n=this.finalize(n,new u.VariableDeclaration(A,"var")),this.nextToken(),E=n,g=this.parseExpression(),n=null}else A.length===1&&A[0].init===null&&this.matchContextualKeyword("of")?(n=this.finalize(n,new u.VariableDeclaration(A,"var")),this.nextToken(),E=n,g=this.parseAssignmentExpression(),n=null,f=!1):(n=this.finalize(n,new u.VariableDeclaration(A,"var")),this.expect(";"))}else if(this.matchKeyword("const")||this.matchKeyword("let")){n=this.createNode();var k=this.nextToken().value;if(!this.context.strict&&this.lookahead.value==="in")n=this.finalize(n,new u.Identifier(k)),this.nextToken(),E=n,g=this.parseExpression(),n=null;else{var F=this.context.allowIn;this.context.allowIn=!1;var A=this.parseBindingList(k,{inFor:!0});this.context.allowIn=F,A.length===1&&A[0].init===null&&this.matchKeyword("in")?(n=this.finalize(n,new u.VariableDeclaration(A,k)),this.nextToken(),E=n,g=this.parseExpression(),n=null):A.length===1&&A[0].init===null&&this.matchContextualKeyword("of")?(n=this.finalize(n,new u.VariableDeclaration(A,k)),this.nextToken(),E=n,g=this.parseAssignmentExpression(),n=null,f=!1):(this.consumeSemicolon(),n=this.finalize(n,new u.VariableDeclaration(A,k)))}}else{var P=this.lookahead,F=this.context.allowIn;if(this.context.allowIn=!1,n=this.inheritCoverGrammar(this.parseAssignmentExpression),this.context.allowIn=F,this.matchKeyword("in"))(!this.context.isAssignmentTarget||n.type===d.Syntax.AssignmentExpression)&&this.tolerateError(c.Messages.InvalidLHSInForIn),this.nextToken(),this.reinterpretExpressionAsPattern(n),E=n,g=this.parseExpression(),n=null;else if(this.matchContextualKeyword("of"))(!this.context.isAssignmentTarget||n.type===d.Syntax.AssignmentExpression)&&this.tolerateError(c.Messages.InvalidLHSInForLoop),this.nextToken(),this.reinterpretExpressionAsPattern(n),E=n,g=this.parseAssignmentExpression(),n=null,f=!1;else{if(this.match(",")){for(var N=[n];this.match(",");)this.nextToken(),N.push(this.isolateCoverGrammar(this.parseAssignmentExpression));n=this.finalize(this.startNode(P),new u.SequenceExpression(N))}this.expect(";")}}typeof E>"u"&&(this.match(";")||(s=this.parseExpression()),this.expect(";"),this.match(")")||(o=this.parseExpression()));var U;if(!this.match(")")&&this.config.tolerant)this.tolerateUnexpectedToken(this.nextToken()),U=this.finalize(this.createNode(),new u.EmptyStatement);else{this.expect(")");var S=this.context.inIteration;this.context.inIteration=!0,U=this.isolateCoverGrammar(this.parseStatement),this.context.inIteration=S}return typeof E>"u"?this.finalize(v,new u.ForStatement(n,s,o,U)):f?this.finalize(v,new u.ForInStatement(E,g,U)):this.finalize(v,new u.ForOfStatement(E,g,U))},l.prototype.parseContinueStatement=function(){var n=this.createNode();this.expectKeyword("continue");var s=null;if(this.lookahead.type===3&&!this.hasLineTerminator){var o=this.parseVariableIdentifier();s=o;var f="$"+o.name;Object.prototype.hasOwnProperty.call(this.context.labelSet,f)||this.throwError(c.Messages.UnknownLabel,o.name)}return this.consumeSemicolon(),s===null&&!this.context.inIteration&&this.throwError(c.Messages.IllegalContinue),this.finalize(n,new u.ContinueStatement(s))},l.prototype.parseBreakStatement=function(){var n=this.createNode();this.expectKeyword("break");var s=null;if(this.lookahead.type===3&&!this.hasLineTerminator){var o=this.parseVariableIdentifier(),f="$"+o.name;Object.prototype.hasOwnProperty.call(this.context.labelSet,f)||this.throwError(c.Messages.UnknownLabel,o.name),s=o}return this.consumeSemicolon(),s===null&&!this.context.inIteration&&!this.context.inSwitch&&this.throwError(c.Messages.IllegalBreak),this.finalize(n,new u.BreakStatement(s))},l.prototype.parseReturnStatement=function(){this.context.inFunctionBody||this.tolerateError(c.Messages.IllegalReturn);var n=this.createNode();this.expectKeyword("return");var s=!this.match(";")&&!this.match("}")&&!this.hasLineTerminator&&this.lookahead.type!==2||this.lookahead.type===8||this.lookahead.type===10,o=s?this.parseExpression():null;return this.consumeSemicolon(),this.finalize(n,new u.ReturnStatement(o))},l.prototype.parseWithStatement=function(){this.context.strict&&this.tolerateError(c.Messages.StrictModeWith);var n=this.createNode(),s;this.expectKeyword("with"),this.expect("(");var o=this.parseExpression();return!this.match(")")&&this.config.tolerant?(this.tolerateUnexpectedToken(this.nextToken()),s=this.finalize(this.createNode(),new u.EmptyStatement)):(this.expect(")"),s=this.parseStatement()),this.finalize(n,new u.WithStatement(o,s))},l.prototype.parseSwitchCase=function(){var n=this.createNode(),s;this.matchKeyword("default")?(this.nextToken(),s=null):(this.expectKeyword("case"),s=this.parseExpression()),this.expect(":");for(var o=[];!(this.match("}")||this.matchKeyword("default")||this.matchKeyword("case"));)o.push(this.parseStatementListItem());return this.finalize(n,new u.SwitchCase(s,o))},l.prototype.parseSwitchStatement=function(){var n=this.createNode();this.expectKeyword("switch"),this.expect("(");var s=this.parseExpression();this.expect(")");var o=this.context.inSwitch;this.context.inSwitch=!0;var f=[],E=!1;for(this.expect("{");!this.match("}");){var g=this.parseSwitchCase();g.test===null&&(E&&this.throwError(c.Messages.MultipleDefaultsInSwitch),E=!0),f.push(g)}return this.expect("}"),this.context.inSwitch=o,this.finalize(n,new u.SwitchStatement(s,f))},l.prototype.parseLabelledStatement=function(){var n=this.createNode(),s=this.parseExpression(),o;if(s.type===d.Syntax.Identifier&&this.match(":")){this.nextToken();var f=s,E="$"+f.name;Object.prototype.hasOwnProperty.call(this.context.labelSet,E)&&this.throwError(c.Messages.Redeclaration,"Label",f.name),this.context.labelSet[E]=!0;var g=void 0;if(this.matchKeyword("class"))this.tolerateUnexpectedToken(this.lookahead),g=this.parseClassDeclaration();else if(this.matchKeyword("function")){var v=this.lookahead,F=this.parseFunctionDeclaration();this.context.strict?this.tolerateUnexpectedToken(v,c.Messages.StrictFunction):F.generator&&this.tolerateUnexpectedToken(v,c.Messages.GeneratorInLegacyContext),g=F}else g=this.parseStatement();delete this.context.labelSet[E],o=new u.LabeledStatement(f,g)}else this.consumeSemicolon(),o=new u.ExpressionStatement(s);return this.finalize(n,o)},l.prototype.parseThrowStatement=function(){var n=this.createNode();this.expectKeyword("throw"),this.hasLineTerminator&&this.throwError(c.Messages.NewlineAfterThrow);var s=this.parseExpression();return this.consumeSemicolon(),this.finalize(n,new u.ThrowStatement(s))},l.prototype.parseCatchClause=function(){var n=this.createNode();this.expectKeyword("catch"),this.expect("("),this.match(")")&&this.throwUnexpectedToken(this.lookahead);for(var s=[],o=this.parsePattern(s),f={},E=0;E0&&this.tolerateError(c.Messages.BadGetterArity);var E=this.parsePropertyMethod(f);return this.context.allowYield=o,this.finalize(n,new u.FunctionExpression(null,f.params,E,s))},l.prototype.parseSetterMethod=function(){var n=this.createNode(),s=!1,o=this.context.allowYield;this.context.allowYield=!s;var f=this.parseFormalParameters();f.params.length!==1?this.tolerateError(c.Messages.BadSetterArity):f.params[0]instanceof u.RestElement&&this.tolerateError(c.Messages.BadSetterRestParameter);var E=this.parsePropertyMethod(f);return this.context.allowYield=o,this.finalize(n,new u.FunctionExpression(null,f.params,E,s))},l.prototype.parseGeneratorMethod=function(){var n=this.createNode(),s=!0,o=this.context.allowYield;this.context.allowYield=!0;var f=this.parseFormalParameters();this.context.allowYield=!1;var E=this.parsePropertyMethod(f);return this.context.allowYield=o,this.finalize(n,new u.FunctionExpression(null,f.params,E,s))},l.prototype.isStartOfExpression=function(){var n=!0,s=this.lookahead.value;switch(this.lookahead.type){case 7:n=s==="["||s==="("||s==="{"||s==="+"||s==="-"||s==="!"||s==="~"||s==="++"||s==="--"||s==="/"||s==="/=";break;case 4:n=s==="class"||s==="delete"||s==="function"||s==="let"||s==="new"||s==="super"||s==="this"||s==="typeof"||s==="void"||s==="yield";break;default:break}return n},l.prototype.parseYieldExpression=function(){var n=this.createNode();this.expectKeyword("yield");var s=null,o=!1;if(!this.hasLineTerminator){var f=this.context.allowYield;this.context.allowYield=!1,o=this.match("*"),o?(this.nextToken(),s=this.parseAssignmentExpression()):this.isStartOfExpression()&&(s=this.parseAssignmentExpression()),this.context.allowYield=f}return this.finalize(n,new u.YieldExpression(s,o))},l.prototype.parseClassElement=function(n){var s=this.lookahead,o=this.createNode(),f="",E=null,g=null,v=!1,F=!1,A=!1,T=!1;if(this.match("*"))this.nextToken();else{v=this.match("["),E=this.parseObjectPropertyKey();var k=E;if(k.name==="static"&&(this.qualifiedPropertyName(this.lookahead)||this.match("*"))&&(s=this.lookahead,A=!0,v=this.match("["),this.match("*")?this.nextToken():E=this.parseObjectPropertyKey()),s.type===3&&!this.hasLineTerminator&&s.value==="async"){var P=this.lookahead.value;P!==":"&&P!=="("&&P!=="*"&&(T=!0,s=this.lookahead,E=this.parseObjectPropertyKey(),s.type===3&&s.value==="constructor"&&this.tolerateUnexpectedToken(s,c.Messages.ConstructorIsAsync))}}var N=this.qualifiedPropertyName(this.lookahead);return s.type===3?s.value==="get"&&N?(f="get",v=this.match("["),E=this.parseObjectPropertyKey(),this.context.allowYield=!1,g=this.parseGetterMethod()):s.value==="set"&&N&&(f="set",v=this.match("["),E=this.parseObjectPropertyKey(),g=this.parseSetterMethod()):s.type===7&&s.value==="*"&&N&&(f="init",v=this.match("["),E=this.parseObjectPropertyKey(),g=this.parseGeneratorMethod(),F=!0),!f&&E&&this.match("(")&&(f="init",g=T?this.parsePropertyMethodAsyncFunction():this.parsePropertyMethodFunction(),F=!0),f||this.throwUnexpectedToken(this.lookahead),f==="init"&&(f="method"),v||(A&&this.isPropertyKey(E,"prototype")&&this.throwUnexpectedToken(s,c.Messages.StaticPrototype),!A&&this.isPropertyKey(E,"constructor")&&((f!=="method"||!F||g&&g.generator)&&this.throwUnexpectedToken(s,c.Messages.ConstructorSpecialMethod),n.value?this.throwUnexpectedToken(s,c.Messages.DuplicateConstructor):n.value=!0,f="constructor")),this.finalize(o,new u.MethodDefinition(E,v,g,f,A))},l.prototype.parseClassElementList=function(){var n=[],s={value:!1};for(this.expect("{");!this.match("}");)this.match(";")?this.nextToken():n.push(this.parseClassElement(s));return this.expect("}"),n},l.prototype.parseClassBody=function(){var n=this.createNode(),s=this.parseClassElementList();return this.finalize(n,new u.ClassBody(s))},l.prototype.parseClassDeclaration=function(n){var s=this.createNode(),o=this.context.strict;this.context.strict=!0,this.expectKeyword("class");var f=n&&this.lookahead.type!==3?null:this.parseVariableIdentifier(),E=null;this.matchKeyword("extends")&&(this.nextToken(),E=this.isolateCoverGrammar(this.parseLeftHandSideExpressionAllowCall));var g=this.parseClassBody();return this.context.strict=o,this.finalize(s,new u.ClassDeclaration(f,E,g))},l.prototype.parseClassExpression=function(){var n=this.createNode(),s=this.context.strict;this.context.strict=!0,this.expectKeyword("class");var o=this.lookahead.type===3?this.parseVariableIdentifier():null,f=null;this.matchKeyword("extends")&&(this.nextToken(),f=this.isolateCoverGrammar(this.parseLeftHandSideExpressionAllowCall));var E=this.parseClassBody();return this.context.strict=s,this.finalize(n,new u.ClassExpression(o,f,E))},l.prototype.parseModule=function(){this.context.strict=!0,this.context.isModule=!0,this.scanner.isModule=!0;for(var n=this.createNode(),s=this.parseDirectivePrologues();this.lookahead.type!==2;)s.push(this.parseStatementListItem());return this.finalize(n,new u.Module(s))},l.prototype.parseScript=function(){for(var n=this.createNode(),s=this.parseDirectivePrologues();this.lookahead.type!==2;)s.push(this.parseStatementListItem());return this.finalize(n,new u.Script(s))},l.prototype.parseModuleSpecifier=function(){var n=this.createNode();this.lookahead.type!==8&&this.throwError(c.Messages.InvalidModuleSpecifier);var s=this.nextToken(),o=this.getTokenRaw(s);return this.finalize(n,new u.Literal(s.value,o))},l.prototype.parseImportSpecifier=function(){var n=this.createNode(),s,o;return this.lookahead.type===3?(s=this.parseVariableIdentifier(),o=s,this.matchContextualKeyword("as")&&(this.nextToken(),o=this.parseVariableIdentifier())):(s=this.parseIdentifierName(),o=s,this.matchContextualKeyword("as")?(this.nextToken(),o=this.parseVariableIdentifier()):this.throwUnexpectedToken(this.nextToken())),this.finalize(n,new u.ImportSpecifier(o,s))},l.prototype.parseNamedImports=function(){this.expect("{");for(var n=[];!this.match("}");)n.push(this.parseImportSpecifier()),this.match("}")||this.expect(",");return this.expect("}"),n},l.prototype.parseImportDefaultSpecifier=function(){var n=this.createNode(),s=this.parseIdentifierName();return this.finalize(n,new u.ImportDefaultSpecifier(s))},l.prototype.parseImportNamespaceSpecifier=function(){var n=this.createNode();this.expect("*"),this.matchContextualKeyword("as")||this.throwError(c.Messages.NoAsAfterImportNamespace),this.nextToken();var s=this.parseIdentifierName();return this.finalize(n,new u.ImportNamespaceSpecifier(s))},l.prototype.parseImportDeclaration=function(){this.context.inFunctionBody&&this.throwError(c.Messages.IllegalImportDeclaration);var n=this.createNode();this.expectKeyword("import");var s,o=[];if(this.lookahead.type===8)s=this.parseModuleSpecifier();else{if(this.match("{")?o=o.concat(this.parseNamedImports()):this.match("*")?o.push(this.parseImportNamespaceSpecifier()):this.isIdentifierName(this.lookahead)&&!this.matchKeyword("default")?(o.push(this.parseImportDefaultSpecifier()),this.match(",")&&(this.nextToken(),this.match("*")?o.push(this.parseImportNamespaceSpecifier()):this.match("{")?o=o.concat(this.parseNamedImports()):this.throwUnexpectedToken(this.lookahead))):this.throwUnexpectedToken(this.nextToken()),!this.matchContextualKeyword("from")){var f=this.lookahead.value?c.Messages.UnexpectedToken:c.Messages.MissingFromClause;this.throwError(f,this.lookahead.value)}this.nextToken(),s=this.parseModuleSpecifier()}return this.consumeSemicolon(),this.finalize(n,new u.ImportDeclaration(o,s))},l.prototype.parseExportSpecifier=function(){var n=this.createNode(),s=this.parseIdentifierName(),o=s;return this.matchContextualKeyword("as")&&(this.nextToken(),o=this.parseIdentifierName()),this.finalize(n,new u.ExportSpecifier(s,o))},l.prototype.parseExportDeclaration=function(){this.context.inFunctionBody&&this.throwError(c.Messages.IllegalExportDeclaration);var n=this.createNode();this.expectKeyword("export");var s;if(this.matchKeyword("default"))if(this.nextToken(),this.matchKeyword("function")){var o=this.parseFunctionDeclaration(!0);s=this.finalize(n,new u.ExportDefaultDeclaration(o))}else if(this.matchKeyword("class")){var o=this.parseClassDeclaration(!0);s=this.finalize(n,new u.ExportDefaultDeclaration(o))}else if(this.matchContextualKeyword("async")){var o=this.matchAsyncFunction()?this.parseFunctionDeclaration(!0):this.parseAssignmentExpression();s=this.finalize(n,new u.ExportDefaultDeclaration(o))}else{this.matchContextualKeyword("from")&&this.throwError(c.Messages.UnexpectedToken,this.lookahead.value);var o=this.match("{")?this.parseObjectInitializer():this.match("[")?this.parseArrayInitializer():this.parseAssignmentExpression();this.consumeSemicolon(),s=this.finalize(n,new u.ExportDefaultDeclaration(o))}else if(this.match("*")){if(this.nextToken(),!this.matchContextualKeyword("from")){var f=this.lookahead.value?c.Messages.UnexpectedToken:c.Messages.MissingFromClause;this.throwError(f,this.lookahead.value)}this.nextToken();var E=this.parseModuleSpecifier();this.consumeSemicolon(),s=this.finalize(n,new u.ExportAllDeclaration(E))}else if(this.lookahead.type===4){var o=void 0;switch(this.lookahead.value){case"let":case"const":o=this.parseLexicalDeclaration({inFor:!1});break;case"var":case"class":case"function":o=this.parseStatementListItem();break;default:this.throwUnexpectedToken(this.lookahead)}s=this.finalize(n,new u.ExportNamedDeclaration(o,[],null))}else if(this.matchAsyncFunction()){var o=this.parseFunctionDeclaration();s=this.finalize(n,new u.ExportNamedDeclaration(o,[],null))}else{var g=[],v=null,F=!1;for(this.expect("{");!this.match("}");)F=F||this.matchKeyword("default"),g.push(this.parseExportSpecifier()),this.match("}")||this.expect(",");if(this.expect("}"),this.matchContextualKeyword("from"))this.nextToken(),v=this.parseModuleSpecifier(),this.consumeSemicolon();else if(F){var f=this.lookahead.value?c.Messages.UnexpectedToken:c.Messages.MissingFromClause;this.throwError(f,this.lookahead.value)}else this.consumeSemicolon();s=this.finalize(n,new u.ExportNamedDeclaration(null,g,v))}return s},l}();e.Parser=m},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});function r(i,a){if(!i)throw new Error("ASSERT: "+a)}e.assert=r},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function i(){this.errors=[],this.tolerant=!1}return i.prototype.recordError=function(a){this.errors.push(a)},i.prototype.tolerate=function(a){if(this.tolerant)this.recordError(a);else throw a},i.prototype.constructError=function(a,c){var u=new Error(a);try{throw u}catch(x){Object.create&&Object.defineProperty&&(u=Object.create(x),Object.defineProperty(u,"column",{value:c}))}return u},i.prototype.createError=function(a,c,u,x){var d="Line "+c+": "+x,p=this.constructError(d,u);return p.index=a,p.lineNumber=c,p.description=x,p},i.prototype.throwError=function(a,c,u,x){throw this.createError(a,c,u,x)},i.prototype.tolerateError=function(a,c,u,x){var d=this.createError(a,c,u,x);if(this.tolerant)this.recordError(d);else throw d},i}();e.ErrorHandler=r},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Messages={BadGetterArity:"Getter must not have any formal parameters",BadSetterArity:"Setter must have exactly one formal parameter",BadSetterRestParameter:"Setter function argument must not be a rest parameter",ConstructorIsAsync:"Class constructor may not be an async method",ConstructorSpecialMethod:"Class constructor may not be an accessor",DeclarationMissingInitializer:"Missing initializer in %0 declaration",DefaultRestParameter:"Unexpected token =",DuplicateBinding:"Duplicate binding %0",DuplicateConstructor:"A class may only have one constructor",DuplicateProtoProperty:"Duplicate __proto__ fields are not allowed in object literals",ForInOfLoopInitializer:"%0 loop variable declaration may not have an initializer",GeneratorInLegacyContext:"Generator declarations are not allowed in legacy contexts",IllegalBreak:"Illegal break statement",IllegalContinue:"Illegal continue statement",IllegalExportDeclaration:"Unexpected token",IllegalImportDeclaration:"Unexpected token",IllegalLanguageModeDirective:"Illegal 'use strict' directive in function with non-simple parameter list",IllegalReturn:"Illegal return statement",InvalidEscapedReservedWord:"Keyword must not contain escaped characters",InvalidHexEscapeSequence:"Invalid hexadecimal escape sequence",InvalidLHSInAssignment:"Invalid left-hand side in assignment",InvalidLHSInForIn:"Invalid left-hand side in for-in",InvalidLHSInForLoop:"Invalid left-hand side in for-loop",InvalidModuleSpecifier:"Unexpected token",InvalidRegExp:"Invalid regular expression",LetInLexicalBinding:"let is disallowed as a lexically bound name",MissingFromClause:"Unexpected token",MultipleDefaultsInSwitch:"More than one default clause in switch statement",NewlineAfterThrow:"Illegal newline after throw",NoAsAfterImportNamespace:"Unexpected token",NoCatchOrFinally:"Missing catch or finally after try",ParameterAfterRestParameter:"Rest parameter must be last formal parameter",Redeclaration:"%0 '%1' has already been declared",StaticPrototype:"Classes may not have static property named prototype",StrictCatchVariable:"Catch variable may not be eval or arguments in strict mode",StrictDelete:"Delete of an unqualified identifier in strict mode.",StrictFunction:"In strict mode code, functions can only be declared at top level or inside a block",StrictFunctionName:"Function name may not be eval or arguments in strict mode",StrictLHSAssignment:"Assignment to eval or arguments is not allowed in strict mode",StrictLHSPostfix:"Postfix increment/decrement may not have eval or arguments operand in strict mode",StrictLHSPrefix:"Prefix increment/decrement may not have eval or arguments operand in strict mode",StrictModeWith:"Strict mode code may not include a with statement",StrictOctalLiteral:"Octal literals are not allowed in strict mode.",StrictParamDupe:"Strict mode function may not have duplicate parameter names",StrictParamName:"Parameter name eval or arguments is not allowed in strict mode",StrictReservedWord:"Use of future reserved word in strict mode",StrictVarName:"Variable name may not be eval or arguments in strict mode",TemplateOctalLiteral:"Octal literals are not allowed in template strings.",UnexpectedEOS:"Unexpected end of input",UnexpectedIdentifier:"Unexpected identifier",UnexpectedNumber:"Unexpected number",UnexpectedReserved:"Unexpected reserved word",UnexpectedString:"Unexpected string",UnexpectedTemplate:"Unexpected quasi %0",UnexpectedToken:"Unexpected token %0",UnexpectedTokenIllegal:"Unexpected token ILLEGAL",UnknownLabel:"Undefined label '%0'",UnterminatedRegExp:"Invalid regular expression: missing /"}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(9),a=r(4),c=r(11);function u(p){return"0123456789abcdef".indexOf(p.toLowerCase())}function x(p){return"01234567".indexOf(p)}var d=function(){function p(h,m){this.source=h,this.errorHandler=m,this.trackComment=!1,this.isModule=!1,this.length=h.length,this.index=0,this.lineNumber=h.length>0?1:0,this.lineStart=0,this.curlyStack=[]}return p.prototype.saveState=function(){return{index:this.index,lineNumber:this.lineNumber,lineStart:this.lineStart}},p.prototype.restoreState=function(h){this.index=h.index,this.lineNumber=h.lineNumber,this.lineStart=h.lineStart},p.prototype.eof=function(){return this.index>=this.length},p.prototype.throwUnexpectedToken=function(h){return h===void 0&&(h=c.Messages.UnexpectedTokenIllegal),this.errorHandler.throwError(this.index,this.lineNumber,this.index-this.lineStart+1,h)},p.prototype.tolerateUnexpectedToken=function(h){h===void 0&&(h=c.Messages.UnexpectedTokenIllegal),this.errorHandler.tolerateError(this.index,this.lineNumber,this.index-this.lineStart+1,h)},p.prototype.skipSingleLineComment=function(h){var m=[],l,n;for(this.trackComment&&(m=[],l=this.index-h,n={start:{line:this.lineNumber,column:this.index-this.lineStart-h},end:{}});!this.eof();){var s=this.source.charCodeAt(this.index);if(++this.index,a.Character.isLineTerminator(s)){if(this.trackComment){n.end={line:this.lineNumber,column:this.index-this.lineStart-1};var o={multiLine:!1,slice:[l+h,this.index-1],range:[l,this.index-1],loc:n};m.push(o)}return s===13&&this.source.charCodeAt(this.index)===10&&++this.index,++this.lineNumber,this.lineStart=this.index,m}}if(this.trackComment){n.end={line:this.lineNumber,column:this.index-this.lineStart};var o={multiLine:!1,slice:[l+h,this.index],range:[l,this.index],loc:n};m.push(o)}return m},p.prototype.skipMultiLineComment=function(){var h=[],m,l;for(this.trackComment&&(h=[],m=this.index-2,l={start:{line:this.lineNumber,column:this.index-this.lineStart-2},end:{}});!this.eof();){var n=this.source.charCodeAt(this.index);if(a.Character.isLineTerminator(n))n===13&&this.source.charCodeAt(this.index+1)===10&&++this.index,++this.lineNumber,++this.index,this.lineStart=this.index;else if(n===42){if(this.source.charCodeAt(this.index+1)===47){if(this.index+=2,this.trackComment){l.end={line:this.lineNumber,column:this.index-this.lineStart};var s={multiLine:!0,slice:[m+2,this.index-2],range:[m,this.index],loc:l};h.push(s)}return h}++this.index}else++this.index}if(this.trackComment){l.end={line:this.lineNumber,column:this.index-this.lineStart};var s={multiLine:!0,slice:[m+2,this.index],range:[m,this.index],loc:l};h.push(s)}return this.tolerateUnexpectedToken(),h},p.prototype.scanComments=function(){var h;this.trackComment&&(h=[]);for(var m=this.index===0;!this.eof();){var l=this.source.charCodeAt(this.index);if(a.Character.isWhiteSpace(l))++this.index;else if(a.Character.isLineTerminator(l))++this.index,l===13&&this.source.charCodeAt(this.index)===10&&++this.index,++this.lineNumber,this.lineStart=this.index,m=!0;else if(l===47)if(l=this.source.charCodeAt(this.index+1),l===47){this.index+=2;var n=this.skipSingleLineComment(2);this.trackComment&&(h=h.concat(n)),m=!0}else if(l===42){this.index+=2;var n=this.skipMultiLineComment();this.trackComment&&(h=h.concat(n))}else break;else if(m&&l===45)if(this.source.charCodeAt(this.index+1)===45&&this.source.charCodeAt(this.index+2)===62){this.index+=3;var n=this.skipSingleLineComment(3);this.trackComment&&(h=h.concat(n))}else break;else if(l===60&&!this.isModule)if(this.source.slice(this.index+1,this.index+4)==="!--"){this.index+=4;var n=this.skipSingleLineComment(4);this.trackComment&&(h=h.concat(n))}else break;else break}return h},p.prototype.isFutureReservedWord=function(h){switch(h){case"enum":case"export":case"import":case"super":return!0;default:return!1}},p.prototype.isStrictModeReservedWord=function(h){switch(h){case"implements":case"interface":case"package":case"private":case"protected":case"public":case"static":case"yield":case"let":return!0;default:return!1}},p.prototype.isRestrictedWord=function(h){return h==="eval"||h==="arguments"},p.prototype.isKeyword=function(h){switch(h.length){case 2:return h==="if"||h==="in"||h==="do";case 3:return h==="var"||h==="for"||h==="new"||h==="try"||h==="let";case 4:return h==="this"||h==="else"||h==="case"||h==="void"||h==="with"||h==="enum";case 5:return h==="while"||h==="break"||h==="catch"||h==="throw"||h==="const"||h==="yield"||h==="class"||h==="super";case 6:return h==="return"||h==="typeof"||h==="delete"||h==="switch"||h==="export"||h==="import";case 7:return h==="default"||h==="finally"||h==="extends";case 8:return h==="function"||h==="continue"||h==="debugger";case 10:return h==="instanceof";default:return!1}},p.prototype.codePointAt=function(h){var m=this.source.charCodeAt(h);if(m>=55296&&m<=56319){var l=this.source.charCodeAt(h+1);if(l>=56320&&l<=57343){var n=m;m=(n-55296)*1024+l-56320+65536}}return m},p.prototype.scanHexEscape=function(h){for(var m=h==="u"?4:2,l=0,n=0;n1114111||h!=="}")&&this.throwUnexpectedToken(),a.Character.fromCodePoint(m)},p.prototype.getIdentifier=function(){for(var h=this.index++;!this.eof();){var m=this.source.charCodeAt(this.index);if(m===92)return this.index=h,this.getComplexIdentifier();if(m>=55296&&m<57343)return this.index=h,this.getComplexIdentifier();if(a.Character.isIdentifierPart(m))++this.index;else break}return this.source.slice(h,this.index)},p.prototype.getComplexIdentifier=function(){var h=this.codePointAt(this.index),m=a.Character.fromCodePoint(h);this.index+=m.length;var l;for(h===92&&(this.source.charCodeAt(this.index)!==117&&this.throwUnexpectedToken(),++this.index,this.source[this.index]==="{"?(++this.index,l=this.scanUnicodeCodePointEscape()):(l=this.scanHexEscape("u"),(l===null||l==="\\"||!a.Character.isIdentifierStart(l.charCodeAt(0)))&&this.throwUnexpectedToken()),m=l);!this.eof()&&(h=this.codePointAt(this.index),!!a.Character.isIdentifierPart(h));)l=a.Character.fromCodePoint(h),m+=l,this.index+=l.length,h===92&&(m=m.substr(0,m.length-1),this.source.charCodeAt(this.index)!==117&&this.throwUnexpectedToken(),++this.index,this.source[this.index]==="{"?(++this.index,l=this.scanUnicodeCodePointEscape()):(l=this.scanHexEscape("u"),(l===null||l==="\\"||!a.Character.isIdentifierPart(l.charCodeAt(0)))&&this.throwUnexpectedToken()),m+=l);return m},p.prototype.octalToDecimal=function(h){var m=h!=="0",l=x(h);return!this.eof()&&a.Character.isOctalDigit(this.source.charCodeAt(this.index))&&(m=!0,l=l*8+x(this.source[this.index++]),"0123".indexOf(h)>=0&&!this.eof()&&a.Character.isOctalDigit(this.source.charCodeAt(this.index))&&(l=l*8+x(this.source[this.index++]))),{code:l,octal:m}},p.prototype.scanIdentifier=function(){var h,m=this.index,l=this.source.charCodeAt(m)===92?this.getComplexIdentifier():this.getIdentifier();if(l.length===1?h=3:this.isKeyword(l)?h=4:l==="null"?h=5:l==="true"||l==="false"?h=1:h=3,h!==3&&m+l.length!==this.index){var n=this.index;this.index=m,this.tolerateUnexpectedToken(c.Messages.InvalidEscapedReservedWord),this.index=n}return{type:h,value:l,lineNumber:this.lineNumber,lineStart:this.lineStart,start:m,end:this.index}},p.prototype.scanPunctuator=function(){var h=this.index,m=this.source[this.index];switch(m){case"(":case"{":m==="{"&&this.curlyStack.push("{"),++this.index;break;case".":++this.index,this.source[this.index]==="."&&this.source[this.index+1]==="."&&(this.index+=2,m="...");break;case"}":++this.index,this.curlyStack.pop();break;case")":case";":case",":case"[":case"]":case":":case"?":case"~":++this.index;break;default:m=this.source.substr(this.index,4),m===">>>="?this.index+=4:(m=m.substr(0,3),m==="==="||m==="!=="||m===">>>"||m==="<<="||m===">>="||m==="**="?this.index+=3:(m=m.substr(0,2),m==="&&"||m==="||"||m==="=="||m==="!="||m==="+="||m==="-="||m==="*="||m==="/="||m==="++"||m==="--"||m==="<<"||m===">>"||m==="&="||m==="|="||m==="^="||m==="%="||m==="<="||m===">="||m==="=>"||m==="**"?this.index+=2:(m=this.source[this.index],"<>=!+-*%&|^/".indexOf(m)>=0&&++this.index)))}return this.index===h&&this.throwUnexpectedToken(),{type:7,value:m,lineNumber:this.lineNumber,lineStart:this.lineStart,start:h,end:this.index}},p.prototype.scanHexLiteral=function(h){for(var m="";!this.eof()&&a.Character.isHexDigit(this.source.charCodeAt(this.index));)m+=this.source[this.index++];return m.length===0&&this.throwUnexpectedToken(),a.Character.isIdentifierStart(this.source.charCodeAt(this.index))&&this.throwUnexpectedToken(),{type:6,value:parseInt("0x"+m,16),lineNumber:this.lineNumber,lineStart:this.lineStart,start:h,end:this.index}},p.prototype.scanBinaryLiteral=function(h){for(var m="",l;!this.eof()&&(l=this.source[this.index],!(l!=="0"&&l!=="1"));)m+=this.source[this.index++];return m.length===0&&this.throwUnexpectedToken(),this.eof()||(l=this.source.charCodeAt(this.index),(a.Character.isIdentifierStart(l)||a.Character.isDecimalDigit(l))&&this.throwUnexpectedToken()),{type:6,value:parseInt(m,2),lineNumber:this.lineNumber,lineStart:this.lineStart,start:h,end:this.index}},p.prototype.scanOctalLiteral=function(h,m){var l="",n=!1;for(a.Character.isOctalDigit(h.charCodeAt(0))?(n=!0,l="0"+this.source[this.index++]):++this.index;!this.eof()&&a.Character.isOctalDigit(this.source.charCodeAt(this.index));)l+=this.source[this.index++];return!n&&l.length===0&&this.throwUnexpectedToken(),(a.Character.isIdentifierStart(this.source.charCodeAt(this.index))||a.Character.isDecimalDigit(this.source.charCodeAt(this.index)))&&this.throwUnexpectedToken(),{type:6,value:parseInt(l,8),octal:n,lineNumber:this.lineNumber,lineStart:this.lineStart,start:m,end:this.index}},p.prototype.isImplicitOctalLiteral=function(){for(var h=this.index+1;h=0&&(n=n.replace(/\\u\{([0-9a-fA-F]+)\}|\\u([a-fA-F0-9]{4})/g,function(o,f,E){var g=parseInt(f||E,16);return g>1114111&&s.throwUnexpectedToken(c.Messages.InvalidRegExp),g<=65535?String.fromCharCode(g):l}).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,l));try{RegExp(n)}catch{this.throwUnexpectedToken(c.Messages.InvalidRegExp)}try{return new RegExp(h,m)}catch{return null}},p.prototype.scanRegExpBody=function(){var h=this.source[this.index];i.assert(h==="/","Regular expression literal must start with a slash");for(var m=this.source[this.index++],l=!1,n=!1;!this.eof();)if(h=this.source[this.index++],m+=h,h==="\\")h=this.source[this.index++],a.Character.isLineTerminator(h.charCodeAt(0))&&this.throwUnexpectedToken(c.Messages.UnterminatedRegExp),m+=h;else if(a.Character.isLineTerminator(h.charCodeAt(0)))this.throwUnexpectedToken(c.Messages.UnterminatedRegExp);else if(l)h==="]"&&(l=!1);else if(h==="/"){n=!0;break}else h==="["&&(l=!0);return n||this.throwUnexpectedToken(c.Messages.UnterminatedRegExp),m.substr(1,m.length-2)},p.prototype.scanRegExpFlags=function(){for(var h="",m="";!this.eof();){var l=this.source[this.index];if(!a.Character.isIdentifierPart(l.charCodeAt(0)))break;if(++this.index,l==="\\"&&!this.eof())if(l=this.source[this.index],l==="u"){++this.index;var n=this.index,s=this.scanHexEscape("u");if(s!==null)for(m+=s,h+="\\u";n=55296&&h<57343&&a.Character.isIdentifierStart(this.codePointAt(this.index))?this.scanIdentifier():this.scanPunctuator()},p}();e.Scanner=d},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TokenName={},e.TokenName[1]="Boolean",e.TokenName[2]="",e.TokenName[3]="Identifier",e.TokenName[4]="Keyword",e.TokenName[5]="Null",e.TokenName[6]="Numeric",e.TokenName[7]="Punctuator",e.TokenName[8]="String",e.TokenName[9]="RegularExpression",e.TokenName[10]="Template"},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.XHTMLEntities={quot:'"',amp:"&",apos:"'",gt:">",nbsp:"\xA0",iexcl:"\xA1",cent:"\xA2",pound:"\xA3",curren:"\xA4",yen:"\xA5",brvbar:"\xA6",sect:"\xA7",uml:"\xA8",copy:"\xA9",ordf:"\xAA",laquo:"\xAB",not:"\xAC",shy:"\xAD",reg:"\xAE",macr:"\xAF",deg:"\xB0",plusmn:"\xB1",sup2:"\xB2",sup3:"\xB3",acute:"\xB4",micro:"\xB5",para:"\xB6",middot:"\xB7",cedil:"\xB8",sup1:"\xB9",ordm:"\xBA",raquo:"\xBB",frac14:"\xBC",frac12:"\xBD",frac34:"\xBE",iquest:"\xBF",Agrave:"\xC0",Aacute:"\xC1",Acirc:"\xC2",Atilde:"\xC3",Auml:"\xC4",Aring:"\xC5",AElig:"\xC6",Ccedil:"\xC7",Egrave:"\xC8",Eacute:"\xC9",Ecirc:"\xCA",Euml:"\xCB",Igrave:"\xCC",Iacute:"\xCD",Icirc:"\xCE",Iuml:"\xCF",ETH:"\xD0",Ntilde:"\xD1",Ograve:"\xD2",Oacute:"\xD3",Ocirc:"\xD4",Otilde:"\xD5",Ouml:"\xD6",times:"\xD7",Oslash:"\xD8",Ugrave:"\xD9",Uacute:"\xDA",Ucirc:"\xDB",Uuml:"\xDC",Yacute:"\xDD",THORN:"\xDE",szlig:"\xDF",agrave:"\xE0",aacute:"\xE1",acirc:"\xE2",atilde:"\xE3",auml:"\xE4",aring:"\xE5",aelig:"\xE6",ccedil:"\xE7",egrave:"\xE8",eacute:"\xE9",ecirc:"\xEA",euml:"\xEB",igrave:"\xEC",iacute:"\xED",icirc:"\xEE",iuml:"\xEF",eth:"\xF0",ntilde:"\xF1",ograve:"\xF2",oacute:"\xF3",ocirc:"\xF4",otilde:"\xF5",ouml:"\xF6",divide:"\xF7",oslash:"\xF8",ugrave:"\xF9",uacute:"\xFA",ucirc:"\xFB",uuml:"\xFC",yacute:"\xFD",thorn:"\xFE",yuml:"\xFF",OElig:"\u0152",oelig:"\u0153",Scaron:"\u0160",scaron:"\u0161",Yuml:"\u0178",fnof:"\u0192",circ:"\u02C6",tilde:"\u02DC",Alpha:"\u0391",Beta:"\u0392",Gamma:"\u0393",Delta:"\u0394",Epsilon:"\u0395",Zeta:"\u0396",Eta:"\u0397",Theta:"\u0398",Iota:"\u0399",Kappa:"\u039A",Lambda:"\u039B",Mu:"\u039C",Nu:"\u039D",Xi:"\u039E",Omicron:"\u039F",Pi:"\u03A0",Rho:"\u03A1",Sigma:"\u03A3",Tau:"\u03A4",Upsilon:"\u03A5",Phi:"\u03A6",Chi:"\u03A7",Psi:"\u03A8",Omega:"\u03A9",alpha:"\u03B1",beta:"\u03B2",gamma:"\u03B3",delta:"\u03B4",epsilon:"\u03B5",zeta:"\u03B6",eta:"\u03B7",theta:"\u03B8",iota:"\u03B9",kappa:"\u03BA",lambda:"\u03BB",mu:"\u03BC",nu:"\u03BD",xi:"\u03BE",omicron:"\u03BF",pi:"\u03C0",rho:"\u03C1",sigmaf:"\u03C2",sigma:"\u03C3",tau:"\u03C4",upsilon:"\u03C5",phi:"\u03C6",chi:"\u03C7",psi:"\u03C8",omega:"\u03C9",thetasym:"\u03D1",upsih:"\u03D2",piv:"\u03D6",ensp:"\u2002",emsp:"\u2003",thinsp:"\u2009",zwnj:"\u200C",zwj:"\u200D",lrm:"\u200E",rlm:"\u200F",ndash:"\u2013",mdash:"\u2014",lsquo:"\u2018",rsquo:"\u2019",sbquo:"\u201A",ldquo:"\u201C",rdquo:"\u201D",bdquo:"\u201E",dagger:"\u2020",Dagger:"\u2021",bull:"\u2022",hellip:"\u2026",permil:"\u2030",prime:"\u2032",Prime:"\u2033",lsaquo:"\u2039",rsaquo:"\u203A",oline:"\u203E",frasl:"\u2044",euro:"\u20AC",image:"\u2111",weierp:"\u2118",real:"\u211C",trade:"\u2122",alefsym:"\u2135",larr:"\u2190",uarr:"\u2191",rarr:"\u2192",darr:"\u2193",harr:"\u2194",crarr:"\u21B5",lArr:"\u21D0",uArr:"\u21D1",rArr:"\u21D2",dArr:"\u21D3",hArr:"\u21D4",forall:"\u2200",part:"\u2202",exist:"\u2203",empty:"\u2205",nabla:"\u2207",isin:"\u2208",notin:"\u2209",ni:"\u220B",prod:"\u220F",sum:"\u2211",minus:"\u2212",lowast:"\u2217",radic:"\u221A",prop:"\u221D",infin:"\u221E",ang:"\u2220",and:"\u2227",or:"\u2228",cap:"\u2229",cup:"\u222A",int:"\u222B",there4:"\u2234",sim:"\u223C",cong:"\u2245",asymp:"\u2248",ne:"\u2260",equiv:"\u2261",le:"\u2264",ge:"\u2265",sub:"\u2282",sup:"\u2283",nsub:"\u2284",sube:"\u2286",supe:"\u2287",oplus:"\u2295",otimes:"\u2297",perp:"\u22A5",sdot:"\u22C5",lceil:"\u2308",rceil:"\u2309",lfloor:"\u230A",rfloor:"\u230B",loz:"\u25CA",spades:"\u2660",clubs:"\u2663",hearts:"\u2665",diams:"\u2666",lang:"\u27E8",rang:"\u27E9"}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(10),a=r(12),c=r(13),u=function(){function d(){this.values=[],this.curly=this.paren=-1}return d.prototype.beforeFunctionExpression=function(p){return["(","{","[","in","typeof","instanceof","new","return","case","delete","throw","void","=","+=","-=","*=","**=","/=","%=","<<=",">>=",">>>=","&=","|=","^=",",","+","-","*","**","/","%","++","--","<<",">>",">>>","&","|","^","!","~","&&","||","?",":","===","==",">=","<=","<",">","!=","!=="].indexOf(p)>=0},d.prototype.isRegexStart=function(){var p=this.values[this.values.length-1],h=p!==null;switch(p){case"this":case"]":h=!1;break;case")":var m=this.values[this.paren-1];h=m==="if"||m==="while"||m==="for"||m==="with";break;case"}":if(h=!1,this.values[this.curly-3]==="function"){var l=this.values[this.curly-4];h=l?!this.beforeFunctionExpression(l):!1}else if(this.values[this.curly-4]==="function"){var l=this.values[this.curly-5];h=l?!this.beforeFunctionExpression(l):!0}break;default:break}return h},d.prototype.push=function(p){p.type===7||p.type===4?(p.value==="{"?this.curly=this.values.length:p.value==="("&&(this.paren=this.values.length),this.values.push(p.value)):this.values.push(null)},d}(),x=function(){function d(p,h){this.errorHandler=new i.ErrorHandler,this.errorHandler.tolerant=h?typeof h.tolerant=="boolean"&&h.tolerant:!1,this.scanner=new a.Scanner(p,this.errorHandler),this.scanner.trackComment=h?typeof h.comment=="boolean"&&h.comment:!1,this.trackRange=h?typeof h.range=="boolean"&&h.range:!1,this.trackLoc=h?typeof h.loc=="boolean"&&h.loc:!1,this.buffer=[],this.reader=new u}return d.prototype.errors=function(){return this.errorHandler.errors},d.prototype.getNextToken=function(){if(this.buffer.length===0){var p=this.scanner.scanComments();if(this.scanner.trackComment)for(var h=0;h{function Bl(t){return Array.isArray?Array.isArray(t):Pt(t)==="[object Array]"}Z.isArray=Bl;function Pl(t){return typeof t=="boolean"}Z.isBoolean=Pl;function Rl(t){return t===null}Z.isNull=Rl;function Il(t){return t==null}Z.isNullOrUndefined=Il;function Nl(t){return typeof t=="number"}Z.isNumber=Nl;function Ll(t){return typeof t=="string"}Z.isString=Ll;function Ol(t){return typeof t=="symbol"}Z.isSymbol=Ol;function Ml(t){return t===void 0}Z.isUndefined=Ml;function Hl(t){return Pt(t)==="[object RegExp]"}Z.isRegExp=Hl;function Xl(t){return typeof t=="object"&&t!==null}Z.isObject=Xl;function Ul(t){return Pt(t)==="[object Date]"}Z.isDate=Ul;function $l(t){return Pt(t)==="[object Error]"||t instanceof Error}Z.isError=$l;function jl(t){return typeof t=="function"}Z.isFunction=jl;function Jl(t){return t===null||typeof t=="boolean"||typeof t=="number"||typeof t=="string"||typeof t=="symbol"||typeof t>"u"}Z.isPrimitive=Jl;Z.isBuffer=O("buffer").Buffer.isBuffer;function Pt(t){return Object.prototype.toString.call(t)}});var Ds=_((tx,gs)=>{var fs=[1,10,100,1e3,1e4,1e5,1e6,1e7,1e8,1e9],I,ps=t=>t<1e5?t<100?t<10?0:1:t<1e4?t<1e3?2:3:4:t<1e7?t<1e6?5:6:t<1e9?t<1e8?7:8:9;function ds(t,e){if(t===e)return 0;if(~~t===t&&~~e===e){if(t===0||e===0)return t=0)return-1;if(t>=0)return 1;t=-t,e=-e}let a=ps(t),c=ps(e),u=0;return ac&&(e*=fs[a-c-1],t/=10,u=1),t===e?u:t=32;)e|=t&1,t>>=1;return t+e}function ms(t,e,r,i){let a=e+1;if(a===r)return 1;if(i(t[a++],t[e])<0){for(;a=0;)a++;return a-e}function xs(t,e,r){for(r--;e>>1;a(c,t[h])<0?d=h:x=h+1}let p=i-x;switch(p){case 3:t[x+3]=t[x+2],I[x+3]=I[x+2];case 2:t[x+2]=t[x+1],I[x+2]=I[x+1];case 1:t[x+1]=t[x],I[x+1]=I[x];break;default:for(;p>0;)t[x+p]=t[x+p-1],I[x+p]=I[x+p-1],p--}t[x]=c,I[x]=u}}function _r(t,e,r,i,a,c){let u=0,x=0,d=1;if(c(t,e[r+a])>0){for(x=i-a;d0;)u=d,d=(d<<1)+1,d<=0&&(d=x);d>x&&(d=x),u+=a,d+=a}else{for(x=a+1;dx&&(d=x);let p=u;u=a-d,d=a-p}for(u++;u>>1);c(t,e[r+p])>0?u=p+1:d=p}return d}function kr(t,e,r,i,a,c){let u=0,x=0,d=1;if(c(t,e[r+a])<0){for(x=a+1;dx&&(d=x);let p=u;u=a-d,d=a-p}else{for(x=i-a;d=0;)u=d,d=(d<<1)+1,d<=0&&(d=x);d>x&&(d=x),u+=a,d+=a}for(u++;u>>1);c(t,e[r+p])<0?d=p:u=p+1}return d}var Tr=class{constructor(e,r){this.array=e,this.compare=r;let{length:i}=e;this.length=i,this.minGallop=7,this.tmpStorageLength=i<2*256?i>>>1:256,this.tmp=new Array(this.tmpStorageLength),this.tmpIndex=new Array(this.tmpStorageLength),this.stackLength=i<120?5:i<1542?10:i<119151?19:40,this.runStart=new Array(this.stackLength),this.runLength=new Array(this.stackLength),this.stackSize=0}pushRun(e,r){this.runStart[this.stackSize]=e,this.runLength[this.stackSize]=r,this.stackSize+=1}mergeRuns(){for(;this.stackSize>1;){let e=this.stackSize-2;if(e>=1&&this.runLength[e-1]<=this.runLength[e]+this.runLength[e+1]||e>=2&&this.runLength[e-2]<=this.runLength[e]+this.runLength[e-1])this.runLength[e-1]this.runLength[e+1])break;this.mergeAt(e)}}forceMergeRuns(){for(;this.stackSize>1;){let e=this.stackSize-2;e>0&&this.runLength[e-1]=7||o>=7);if(f)break;n<0&&(n=0),n+=2}if(this.minGallop=n,n<1&&(this.minGallop=1),r===1){for(p=0;p=0;p--)u[s+p]=u[n+p],I[s+p]=I[n+p];u[l]=x[m],I[l]=d[m];return}let{minGallop:o}=this;for(;;){let f=0,E=0,g=!1;do if(c(x[m],u[h])<0){if(u[l]=u[h],I[l]=I[h],l--,h--,f++,E=0,--r===0){g=!0;break}}else if(u[l]=x[m],I[l]=d[m],l--,m--,E++,f=0,--a===1){g=!0;break}while((f|E)=0;p--)u[s+p]=u[n+p],I[s+p]=I[n+p];if(r===0){g=!0;break}}if(u[l]=x[m],I[l]=d[m],l--,m--,--a===1){g=!0;break}if(E=a-_r(u[h],x,0,a,a-1,c),E!==0){for(l-=E,m-=E,a-=E,s=l+1,n=m+1,p=0;p=7||E>=7);if(g)break;o<0&&(o=0),o+=2}if(this.minGallop=o,o<1&&(this.minGallop=1),a===1){for(l-=r,h-=r,s=l+1,n=h+1,p=r-1;p>=0;p--)u[s+p]=u[n+p],I[s+p]=I[n+p];u[l]=x[m],I[l]=d[m]}else{if(a===0)throw new Error("mergeHigh preconditions were not respected");for(n=l-(a-1),p=0;pp&&(h=p),Es(t,r,r+h,r+x,e),x=h}d.pushRun(r,x),d.mergeRuns(),u-=x,r+=x}while(u!==0);return d.forceMergeRuns(),I}gs.exports={sort:ql}});var vs=_((rx,ys)=>{"use strict";var Gl=Object.prototype.hasOwnProperty;ys.exports=(t,e)=>Gl.call(t,e)});var xt=_((ix,Ps)=>{var Rr=vs(),{isObject:Ss,isArray:Kl,isString:Wl,isNumber:Vl}=Rt(),Ir="before",Cs="after-prop",bs="after-colon",Fs="after-value",ws="after",_s="before-all",ks="after-all",Yl="[",Ql="]",Zl="{",eh="}",th=",",rh="",ih="-",Nr=[Ir,Cs,bs,Fs,ws],nh=[Ir,_s,ks].map(Symbol.for),Ts=":",As=void 0,mt=(t,e)=>Symbol.for(t+Ts+e),It=(t,e,r)=>Object.defineProperty(t,e,{value:r,writable:!0,configurable:!0}),Pr=(t,e,r,i,a,c)=>{let u=mt(a,i);if(!Rr(e,u))return;let x=r===i?u:mt(a,r);It(t,x,e[u]),c&&delete e[u]},Bs=(t,e,r,i,a)=>{Nr.forEach(c=>{Pr(t,e,r,i,c,a)})},sh=(t,e,r)=>{e!==r&&Nr.forEach(i=>{let a=mt(i,r);if(!Rr(t,a)){Pr(t,t,r,e,i,!0);return}let c=t[a];delete t[a],Pr(t,t,r,e,i,!0),It(t,mt(i,e),c)})},Br=(t,e)=>{nh.forEach(r=>{let i=e[r];i&&It(t,r,i)})},ah=(t,e,r)=>(r.forEach(i=>{!Wl(i)&&!Vl(i)||Rr(e,i)&&(t[i]=e[i],Bs(t,e,i,i))}),t);Ps.exports={SYMBOL_PREFIXES:Nr,PREFIX_BEFORE:Ir,PREFIX_AFTER_PROP:Cs,PREFIX_AFTER_COLON:bs,PREFIX_AFTER_VALUE:Fs,PREFIX_AFTER:ws,PREFIX_BEFORE_ALL:_s,PREFIX_AFTER_ALL:ks,BRACKET_OPEN:Yl,BRACKET_CLOSE:Ql,CURLY_BRACKET_OPEN:Zl,CURLY_BRACKET_CLOSE:eh,COLON:Ts,COMMA:th,MINUS:ih,EMPTY:rh,UNDEFINED:As,symbol:mt,define:It,copy_comments:Bs,swap_comments:sh,assign_non_prop_comments:Br,assign(t,e,r){if(!Ss(t))throw new TypeError("Cannot convert undefined or null to object");if(!Ss(e))return t;if(r===As)r=Object.keys(e),Br(t,e);else if(Kl(r))r.length===0&&Br(t,e);else throw new TypeError("keys must be array or undefined");return ah(t,e,r)}}});var Or=_((nx,Os)=>{var{isArray:oh}=Rt(),{sort:uh}=Ds(),{SYMBOL_PREFIXES:ch,UNDEFINED:Rs,symbol:lh,copy_comments:hh,swap_comments:Ls}=xt(),fh=t=>{let{length:e}=t,r=0,i=e/2;for(;r{hh(t,e,r+i,r,a)},tt=(t,e,r,i,a,c)=>{if(a>0){let x=i;for(;x-- >0;)Is(t,e,r+x,a,c);return}let u=0;for(;u{ch.forEach(r=>{let i=lh(r,e);delete t[i]})},ph=(t,e)=>{let r=e;for(;r in t;)r=t[r];return r},Lr=class t extends Array{splice(...e){let{length:r}=this,i=super.splice(...e),[a,c,...u]=e;a<0&&(a+=r),arguments.length===1?c=r-a:c=Math.min(r-a,c);let{length:x}=u,d=x-c,p=a+c,h=r-p;return tt(this,this,p,h,d,!0),i}slice(...e){let{length:r}=this,i=super.slice(...e);if(!i.length)return new t;let[a,c]=e;return c===Rs?c=r:c<0&&(c+=r),a<0?a+=r:a===Rs&&(a=0),tt(i,this,a,c-a,-a),i}unshift(...e){let{length:r}=this,i=super.unshift(...e),{length:a}=e;return a>0&&tt(this,this,0,r,a,!0),i}shift(){let e=super.shift(),{length:r}=this;return Ns(this,0),tt(this,this,1,r,-1,!0),e}reverse(){return super.reverse(),fh(this),this}pop(){let e=super.pop();return Ns(this,this.length),e}concat(...e){let{length:r}=this,i=super.concat(...e);return e.length&&(tt(i,this,0,this.length,0),e.forEach(a=>{let c=r;r+=oh(a)?a.length:1,a instanceof t&&tt(i,a,0,a.length,c)})),i}sort(...e){let r=uh(this,...e.slice(0,1)),i=Object.create(null);return r.forEach((a,c)=>{if(a===c)return;let u=ph(i,a);u!==c&&(i[c]=u,Ls(this,c,u))}),this}};Os.exports={CommentArray:Lr}});var ea=_((sx,Zs)=>{var dh=hs(),{CommentArray:mh}=Or(),{PREFIX_BEFORE:Lt,PREFIX_AFTER_PROP:xh,PREFIX_AFTER_COLON:Eh,PREFIX_AFTER_VALUE:Us,PREFIX_AFTER:Hr,PREFIX_BEFORE_ALL:gh,PREFIX_AFTER_ALL:Dh,BRACKET_OPEN:yh,BRACKET_CLOSE:Ms,CURLY_BRACKET_OPEN:vh,CURLY_BRACKET_CLOSE:Hs,COLON:$s,COMMA:js,MINUS:Xs,EMPTY:Sh,UNDEFINED:Ht,define:Xr,assign_non_prop_comments:Ah}=xt(),Js=t=>dh.tokenize(t,{comment:!0,loc:!0}),Ur=[],Re=null,ve=null,$r=[],Ie,zs=!1,qs=!1,Et=null,gt=null,ee=null,Gs,Ot=null,Ks=()=>{$r.length=Ur.length=0,gt=null,Ie=Ht},Ch=()=>{Ks(),Et.length=0,ve=Re=Et=gt=ee=Ot=null},jr=t=>Symbol.for(Ie!==Ht?t+$s+Ie:t),Jr=(t,e)=>Ot?Ot(t,e):e,Ws=()=>{let t=new SyntaxError(`Unexpected token ${ee.value.slice(0,1)}`);throw Object.assign(t,ee.loc.start),t},Vs=()=>{let t=new SyntaxError("Unexpected end of JSON input");throw Object.assign(t,gt?gt.loc.end:{line:1,column:0}),t},pe=()=>{let t=Et[++Gs];qs=ee&&t&&ee.loc.end.line===t.loc.start.line||!1,gt=ee,ee=t},Mr=()=>(ee||Vs(),ee.type==="Punctuator"?ee.value:ee.type),je=t=>Mr()===t,Nt=t=>{je(t)||Ws()},zr=t=>{Ur.push(Re),Re=t},qr=()=>{Re=Ur.pop()},Ys=()=>{if(!ve)return;let t=[];for(let r of ve)if(r.inline)t.push(r);else break;let{length:e}=t;e&&(e===ve.length?ve=null:ve.splice(0,e),Xr(Re,jr(Hr),t))},Pe=t=>{ve&&(Xr(Re,jr(t),ve),ve=null)},Se=t=>{let e=[];for(;ee&&(je("LineComment")||je("BlockComment"));){let r={...ee,inline:qs};e.push(r),pe()}if(!zs&&e.length){if(t){Xr(Re,jr(t),e);return}ve=e}},Mt=(t,e)=>{e&&$r.push(Ie),Ie=t},Qs=()=>{Ie=$r.pop()},bh=()=>{let t={};zr(t),Mt(Ht,!0);let e=!1,r;for(Se();!je(Hs)&&!(e&&(Pe(Us),Nt(js),pe(),Se(),Ys(),je(Hs)));)e=!0,Nt("String"),r=JSON.parse(ee.value),Mt(r),Pe(Lt),pe(),Se(xh),Nt($s),pe(),Se(Eh),t[r]=Jr(r,Gr()),Se();return e&&Pe(Hr),pe(),Ie=void 0,e||Pe(Lt),qr(),Qs(),t},Fh=()=>{let t=new mh;zr(t),Mt(Ht,!0);let e=!1,r=0;for(Se();!je(Ms)&&!(e&&(Pe(Us),Nt(js),pe(),Se(),Ys(),je(Ms)));)e=!0,Mt(r),Pe(Lt),t[r]=Jr(r,Gr()),r++,Se();return e&&Pe(Hr),pe(),Ie=void 0,e||Pe(Lt),qr(),Qs(),t};function Gr(){let t=Mr();if(t===vh)return pe(),bh();if(t===yh)return pe(),Fh();let e=Sh;t===Xs&&(pe(),t=Mr(),e=Xs);let r;switch(t){case"String":case"Boolean":case"Null":case"Numeric":return r=ee.value,pe(),JSON.parse(e+r);default:}}var wh=t=>Object(t)===t,_h=(t,e,r)=>{Ks(),Et=Js(t),Ot=e,zs=r,Et.length||Vs(),Gs=-1,pe(),zr({}),Se(gh);let i=Gr();return Se(Dh),ee&&Ws(),!r&&i!==null&&(wh(i)||(i=new Object(i)),Ah(i,Re)),qr(),i=Jr("",i),Ch(),i};Zs.exports={parse:_h,tokenize:Js}});var ra=_((ax,ta)=>{"use strict";var _e="",Kr;ta.exports=kh;function kh(t,e){if(typeof t!="string")throw new TypeError("expected a string");if(e===1)return t;if(e===2)return t+t;var r=t.length*e;if(Kr!==t||typeof Kr>"u")Kr=t,_e="";else if(_e.length>=r)return _e.substr(0,r);for(;r>_e.length&&e>1;)e&1&&(_e+=t),e>>=1,t+=t;return _e+=t,_e=_e.substr(0,r),_e}});var pa=_((ox,fa)=>{var{isArray:Yr,isObject:ia,isFunction:Vr,isNumber:Th,isString:Bh}=Rt(),Ph=ra(),{PREFIX_BEFORE_ALL:Rh,PREFIX_BEFORE:na,PREFIX_AFTER_PROP:Ih,PREFIX_AFTER_COLON:Nh,PREFIX_AFTER_VALUE:Lh,PREFIX_AFTER:Qr,PREFIX_AFTER_ALL:Oh,BRACKET_OPEN:Mh,BRACKET_CLOSE:Hh,CURLY_BRACKET_OPEN:Xh,CURLY_BRACKET_CLOSE:Uh,COLON:$h,COMMA:sa,EMPTY:ue,UNDEFINED:jh}=xt(),Wr=/[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,Zr=" ",Je=` +`,aa="null",oa=t=>`${na}:${t}`,Jh=t=>`${Ih}:${t}`,zh=t=>`${Nh}:${t}`,ua=t=>`${Lh}:${t}`,ca=t=>`${Qr}:${t}`,qh={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},Gh=t=>(Wr.lastIndex=0,Wr.test(t)?t.replace(Wr,e=>{let r=qh[e];return typeof r=="string"?r:e}):t),la=t=>`"${Gh(t)}"`,Kh=(t,e)=>e?`//${t}`:`/*${t}*/`,oe=(t,e,r,i)=>{let a=t[Symbol.for(e)];if(!a||!a.length)return ue;let c=!1,u=a.reduce((x,{inline:d,type:p,value:h})=>{let m=d?Zr:Je+r;return c=p==="LineComment",x+m+Kh(h,c)},ue);return i||c?u+Je+r:u},rt=null,yt=ue,Wh=()=>{rt=null,yt=ue},Dt=(t,e,r)=>t?e?t+e.trim()+Je+r:t.trimRight()+Je+r:e?e.trimRight()+Je+r:ue,ha=(t,e,r)=>{let i=oe(e,na,r+yt,!0);return Dt(i,t,r)},Vh=(t,e)=>{let r=e+yt,{length:i}=t,a=ue,c=ue;for(let u=0;u{if(!t)return"null";let r=e+yt,i=ue,a=ue,c=!0,u=Yr(rt)?rt:Object.keys(t),x=d=>{let p=ei(d,t,r);if(p===jh)return;c||(i+=sa),c=!1;let h=Dt(a,oe(t,oa(d),r),r);i+=h||Je+r,i+=la(d)+oe(t,Jh(d),r)+$h+oe(t,zh(d),r)+Zr+p+oe(t,ua(d),r),a=oe(t,ca(d),r)};return u.forEach(x),i+=Dt(a,oe(t,Qr,r),r),Xh+ha(i,t,e)+Uh};function ei(t,e,r){let i=e[t];switch(ia(i)&&Vr(i.toJSON)&&(i=i.toJSON(t)),Vr(rt)&&(i=rt.call(e,t,i)),typeof i){case"string":return la(i);case"number":return Number.isFinite(i)?String(i):aa;case"boolean":case"null":return String(i);case"object":return Yr(i)?Vh(i,r):Yh(i,r);default:}}var Qh=t=>Bh(t)?t:Th(t)?Ph(Zr,t):ue,{toString:Zh}=Object.prototype,ef=["[object Number]","[object String]","[object Boolean]"],tf=t=>{if(typeof t!="object")return!1;let e=Zh.call(t);return ef.includes(e)};fa.exports=(t,e,r)=>{let i=Qh(r);if(!i)return JSON.stringify(t,e);!Vr(e)&&!Yr(e)&&(e=null),rt=e,yt=i;let a=tf(t)?JSON.stringify(t):ei("",{"":t},ue);return Wh(),ia(t)?oe(t,Rh,ue).trimLeft()+a+oe(t,Oh,ue).trimRight():a}});var ti=_((ux,da)=>{var{parse:rf,tokenize:nf}=ea(),sf=pa(),{CommentArray:af}=Or(),{assign:of}=xt();da.exports={parse:rf,stringify:sf,tokenize:nf,CommentArray:af,assign:of}});var Sa=_(it=>{"use strict";Object.defineProperty(it,"__esModule",{value:!0});it.splitWhen=it.flatten=void 0;function Ef(t){return t.reduce((e,r)=>[].concat(e,r),[])}it.flatten=Ef;function gf(t,e){let r=[[]],i=0;for(let a of t)e(a)?(i++,r[i]=[]):r[i].push(a);return r}it.splitWhen=gf});var Aa=_($t=>{"use strict";Object.defineProperty($t,"__esModule",{value:!0});$t.isEnoentCodeError=void 0;function Df(t){return t.code==="ENOENT"}$t.isEnoentCodeError=Df});var Ca=_(jt=>{"use strict";Object.defineProperty(jt,"__esModule",{value:!0});jt.createDirentFromStats=void 0;var oi=class{constructor(e,r){this.name=e,this.isBlockDevice=r.isBlockDevice.bind(r),this.isCharacterDevice=r.isCharacterDevice.bind(r),this.isDirectory=r.isDirectory.bind(r),this.isFIFO=r.isFIFO.bind(r),this.isFile=r.isFile.bind(r),this.isSocket=r.isSocket.bind(r),this.isSymbolicLink=r.isSymbolicLink.bind(r)}};function yf(t,e){return new oi(t,e)}jt.createDirentFromStats=yf});var _a=_(V=>{"use strict";Object.defineProperty(V,"__esModule",{value:!0});V.convertPosixPathToPattern=V.convertWindowsPathToPattern=V.convertPathToPattern=V.escapePosixPath=V.escapeWindowsPath=V.escape=V.removeLeadingDotSegment=V.makeAbsolute=V.unixify=void 0;var vf=O("os"),Sf=O("path"),ba=vf.platform()==="win32",Af=2,Cf=/(\\?)([()*?[\]{|}]|^!|[!+@](?=\()|\\(?![!()*+?@[\]{|}]))/g,bf=/(\\?)([()[\]{}]|^!|[!+@](?=\())/g,Ff=/^\\\\([.?])/,wf=/\\(?![!()+@[\]{}])/g;function _f(t){return t.replace(/\\/g,"/")}V.unixify=_f;function kf(t,e){return Sf.resolve(t,e)}V.makeAbsolute=kf;function Tf(t){if(t.charAt(0)==="."){let e=t.charAt(1);if(e==="/"||e==="\\")return t.slice(Af)}return t}V.removeLeadingDotSegment=Tf;V.escape=ba?ui:ci;function ui(t){return t.replace(bf,"\\$2")}V.escapeWindowsPath=ui;function ci(t){return t.replace(Cf,"\\$2")}V.escapePosixPath=ci;V.convertPathToPattern=ba?Fa:wa;function Fa(t){return ui(t).replace(Ff,"//$1").replace(wf,"/")}V.convertWindowsPathToPattern=Fa;function wa(t){return ci(t)}V.convertPosixPathToPattern=wa});var Ta=_((dx,ka)=>{ka.exports=function(e){if(typeof e!="string"||e==="")return!1;for(var r;r=/(\\).|([@?!+*]\(.*\))/g.exec(e);){if(r[2])return!0;e=e.slice(r.index+r[0].length)}return!1}});var Ra=_((mx,Pa)=>{var Bf=Ta(),Ba={"{":"}","(":")","[":"]"},Pf=function(t){if(t[0]==="!")return!0;for(var e=0,r=-2,i=-2,a=-2,c=-2,u=-2;ee&&(u===-1||u>i||(u=t.indexOf("\\",e),u===-1||u>i)))||a!==-1&&t[e]==="{"&&t[e+1]!=="}"&&(a=t.indexOf("}",e),a>e&&(u=t.indexOf("\\",e),u===-1||u>a))||c!==-1&&t[e]==="("&&t[e+1]==="?"&&/[:!=]/.test(t[e+2])&&t[e+3]!==")"&&(c=t.indexOf(")",e),c>e&&(u=t.indexOf("\\",e),u===-1||u>c))||r!==-1&&t[e]==="("&&t[e+1]!=="|"&&(rr&&(u=t.indexOf("\\",r),u===-1||u>c))))return!0;if(t[e]==="\\"){var x=t[e+1];e+=2;var d=Ba[x];if(d){var p=t.indexOf(d,e);p!==-1&&(e=p+1)}if(t[e]==="!")return!0}else e++}return!1},Rf=function(t){if(t[0]==="!")return!0;for(var e=0;e{"use strict";var If=Ra(),Nf=O("path").posix.dirname,Lf=O("os").platform()==="win32",li="/",Of=/\\/g,Mf=/[\{\[].*[\}\]]$/,Hf=/(^|[^\\])([\{\[]|\([^\)]+$)/,Xf=/\\([\!\*\?\|\[\]\(\)\{\}])/g;Ia.exports=function(e,r){var i=Object.assign({flipBackslashes:!0},r);i.flipBackslashes&&Lf&&e.indexOf(li)<0&&(e=e.replace(Of,li)),Mf.test(e)&&(e+=li),e+="a";do e=Nf(e);while(If(e)||Hf.test(e));return e.replace(Xf,"$1")}});var Jt=_(de=>{"use strict";de.isInteger=t=>typeof t=="number"?Number.isInteger(t):typeof t=="string"&&t.trim()!==""?Number.isInteger(Number(t)):!1;de.find=(t,e)=>t.nodes.find(r=>r.type===e);de.exceedsLimit=(t,e,r=1,i)=>i===!1||!de.isInteger(t)||!de.isInteger(e)?!1:(Number(e)-Number(t))/Number(r)>=i;de.escapeNode=(t,e=0,r)=>{let i=t.nodes[e];i&&(r&&i.type===r||i.type==="open"||i.type==="close")&&i.escaped!==!0&&(i.value="\\"+i.value,i.escaped=!0)};de.encloseBrace=t=>t.type!=="brace"||t.commas>>0+t.ranges>>0?!1:(t.invalid=!0,!0);de.isInvalidBrace=t=>t.type!=="brace"?!1:t.invalid===!0||t.dollar?!0:!(t.commas>>0+t.ranges>>0)||t.open!==!0||t.close!==!0?(t.invalid=!0,!0):!1;de.isOpenOrClose=t=>t.type==="open"||t.type==="close"?!0:t.open===!0||t.close===!0;de.reduce=t=>t.reduce((e,r)=>(r.type==="text"&&e.push(r.value),r.type==="range"&&(r.type="text"),e),[]);de.flatten=(...t)=>{let e=[],r=i=>{for(let a=0;a{"use strict";var La=Jt();Oa.exports=(t,e={})=>{let r=(i,a={})=>{let c=e.escapeInvalid&&La.isInvalidBrace(a),u=i.invalid===!0&&e.escapeInvalid===!0,x="";if(i.value)return(c||u)&&La.isOpenOrClose(i)?"\\"+i.value:i.value;if(i.value)return i.value;if(i.nodes)for(let d of i.nodes)x+=r(d);return x};return r(t)}});var Ha=_((Dx,Ma)=>{"use strict";Ma.exports=function(t){return typeof t=="number"?t-t===0:typeof t=="string"&&t.trim()!==""?Number.isFinite?Number.isFinite(+t):isFinite(+t):!1}});var Ka=_((yx,Ga)=>{"use strict";var Xa=Ha(),ze=(t,e,r)=>{if(Xa(t)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(e===void 0||t===e)return String(t);if(Xa(e)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let i={relaxZeros:!0,...r};typeof i.strictZeros=="boolean"&&(i.relaxZeros=i.strictZeros===!1);let a=String(i.relaxZeros),c=String(i.shorthand),u=String(i.capture),x=String(i.wrap),d=t+":"+e+"="+a+c+u+x;if(ze.cache.hasOwnProperty(d))return ze.cache[d].result;let p=Math.min(t,e),h=Math.max(t,e);if(Math.abs(p-h)===1){let o=t+"|"+e;return i.capture?`(${o})`:i.wrap===!1?o:`(?:${o})`}let m=qa(t)||qa(e),l={min:t,max:e,a:p,b:h},n=[],s=[];if(m&&(l.isPadded=m,l.maxLen=String(l.max).length),p<0){let o=h<0?Math.abs(h):1;s=Ua(o,Math.abs(p),l,i),p=l.a=0}return h>=0&&(n=Ua(p,h,l,i)),l.negatives=s,l.positives=n,l.result=Uf(s,n,i),i.capture===!0?l.result=`(${l.result})`:i.wrap!==!1&&n.length+s.length>1&&(l.result=`(?:${l.result})`),ze.cache[d]=l,l.result};function Uf(t,e,r){let i=hi(t,e,"-",!1,r)||[],a=hi(e,t,"",!1,r)||[],c=hi(t,e,"-?",!0,r)||[];return i.concat(c).concat(a).join("|")}function $f(t,e){let r=1,i=1,a=ja(t,r),c=new Set([e]);for(;t<=a&&a<=e;)c.add(a),r+=1,a=ja(t,r);for(a=Ja(e+1,i)-1;t1&&x.count.pop(),x.count.push(h.count[0]),x.string=x.pattern+za(x.count),u=p+1;continue}r.isPadded&&(m=Gf(p,r,i)),h.string=m+h.pattern+za(h.count),c.push(h),u=p+1,x=h}return c}function hi(t,e,r,i,a){let c=[];for(let u of t){let{string:x}=u;!i&&!$a(e,"string",x)&&c.push(r+x),i&&$a(e,"string",x)&&c.push(r+x)}return c}function Jf(t,e){let r=[];for(let i=0;ie?1:e>t?-1:0}function $a(t,e,r){return t.some(i=>i[e]===r)}function ja(t,e){return Number(String(t).slice(0,-e)+"9".repeat(e))}function Ja(t,e){return t-t%Math.pow(10,e)}function za(t){let[e=0,r=""]=t;return r||e>1?`{${e+(r?","+r:"")}}`:""}function qf(t,e,r){return`[${t}${e-t===1?"":"-"}${e}]`}function qa(t){return/^-?(0+)\d/.test(t)}function Gf(t,e,r){if(!e.isPadded)return t;let i=Math.abs(e.maxLen-String(t).length),a=r.relaxZeros!==!1;switch(i){case 0:return"";case 1:return a?"0?":"0";case 2:return a?"0{0,2}":"00";default:return a?`0{0,${i}}`:`0{${i}}`}}ze.cache={};ze.clearCache=()=>ze.cache={};Ga.exports=ze});var di=_((vx,to)=>{"use strict";var Kf=O("util"),Va=Ka(),Wa=t=>t!==null&&typeof t=="object"&&!Array.isArray(t),Wf=t=>e=>t===!0?Number(e):String(e),fi=t=>typeof t=="number"||typeof t=="string"&&t!=="",vt=t=>Number.isInteger(+t),pi=t=>{let e=`${t}`,r=-1;if(e[0]==="-"&&(e=e.slice(1)),e==="0")return!1;for(;e[++r]==="0";);return r>0},Vf=(t,e,r)=>typeof t=="string"||typeof e=="string"?!0:r.stringify===!0,Yf=(t,e,r)=>{if(e>0){let i=t[0]==="-"?"-":"";i&&(t=t.slice(1)),t=i+t.padStart(i?e-1:e,"0")}return r===!1?String(t):t},Gt=(t,e)=>{let r=t[0]==="-"?"-":"";for(r&&(t=t.slice(1),e--);t.length{t.negatives.sort((x,d)=>xd?1:0),t.positives.sort((x,d)=>xd?1:0);let i=e.capture?"":"?:",a="",c="",u;return t.positives.length&&(a=t.positives.map(x=>Gt(String(x),r)).join("|")),t.negatives.length&&(c=`-(${i}${t.negatives.map(x=>Gt(String(x),r)).join("|")})`),a&&c?u=`${a}|${c}`:u=a||c,e.wrap?`(${i}${u})`:u},Ya=(t,e,r,i)=>{if(r)return Va(t,e,{wrap:!1,...i});let a=String.fromCharCode(t);if(t===e)return a;let c=String.fromCharCode(e);return`[${a}-${c}]`},Qa=(t,e,r)=>{if(Array.isArray(t)){let i=r.wrap===!0,a=r.capture?"":"?:";return i?`(${a}${t.join("|")})`:t.join("|")}return Va(t,e,r)},Za=(...t)=>new RangeError("Invalid range arguments: "+Kf.inspect(...t)),eo=(t,e,r)=>{if(r.strictRanges===!0)throw Za([t,e]);return[]},Zf=(t,e)=>{if(e.strictRanges===!0)throw new TypeError(`Expected step "${t}" to be a number`);return[]},ep=(t,e,r=1,i={})=>{let a=Number(t),c=Number(e);if(!Number.isInteger(a)||!Number.isInteger(c)){if(i.strictRanges===!0)throw Za([t,e]);return[]}a===0&&(a=0),c===0&&(c=0);let u=a>c,x=String(t),d=String(e),p=String(r);r=Math.max(Math.abs(r),1);let h=pi(x)||pi(d)||pi(p),m=h?Math.max(x.length,d.length,p.length):0,l=h===!1&&Vf(t,e,i)===!1,n=i.transform||Wf(l);if(i.toRegex&&r===1)return Ya(Gt(t,m),Gt(e,m),!0,i);let s={negatives:[],positives:[]},o=g=>s[g<0?"negatives":"positives"].push(Math.abs(g)),f=[],E=0;for(;u?a>=c:a<=c;)i.toRegex===!0&&r>1?o(a):f.push(Yf(n(a,E),m,l)),a=u?a-r:a+r,E++;return i.toRegex===!0?r>1?Qf(s,i,m):Qa(f,null,{wrap:!1,...i}):f},tp=(t,e,r=1,i={})=>{if(!vt(t)&&t.length>1||!vt(e)&&e.length>1)return eo(t,e,i);let a=i.transform||(l=>String.fromCharCode(l)),c=`${t}`.charCodeAt(0),u=`${e}`.charCodeAt(0),x=c>u,d=Math.min(c,u),p=Math.max(c,u);if(i.toRegex&&r===1)return Ya(d,p,!1,i);let h=[],m=0;for(;x?c>=u:c<=u;)h.push(a(c,m)),c=x?c-r:c+r,m++;return i.toRegex===!0?Qa(h,null,{wrap:!1,options:i}):h},qt=(t,e,r,i={})=>{if(e==null&&fi(t))return[t];if(!fi(t)||!fi(e))return eo(t,e,i);if(typeof r=="function")return qt(t,e,1,{transform:r});if(Wa(r))return qt(t,e,0,r);let a={...i};return a.capture===!0&&(a.wrap=!0),r=r||a.step||1,vt(r)?vt(t)&&vt(e)?ep(t,e,r,a):tp(t,e,Math.max(Math.abs(r),1),a):r!=null&&!Wa(r)?Zf(r,a):qt(t,e,1,r)};to.exports=qt});var no=_((Sx,io)=>{"use strict";var rp=di(),ro=Jt(),ip=(t,e={})=>{let r=(i,a={})=>{let c=ro.isInvalidBrace(a),u=i.invalid===!0&&e.escapeInvalid===!0,x=c===!0||u===!0,d=e.escapeInvalid===!0?"\\":"",p="";if(i.isOpen===!0)return d+i.value;if(i.isClose===!0)return console.log("node.isClose",d,i.value),d+i.value;if(i.type==="open")return x?d+i.value:"(";if(i.type==="close")return x?d+i.value:")";if(i.type==="comma")return i.prev.type==="comma"?"":x?i.value:"|";if(i.value)return i.value;if(i.nodes&&i.ranges>0){let h=ro.reduce(i.nodes),m=rp(...h,{...e,wrap:!1,toRegex:!0,strictZeros:!0});if(m.length!==0)return h.length>1&&m.length>1?`(${m})`:m}if(i.nodes)for(let h of i.nodes)p+=r(h,i);return p};return r(t)};io.exports=ip});var oo=_((Ax,ao)=>{"use strict";var np=di(),so=zt(),nt=Jt(),qe=(t="",e="",r=!1)=>{let i=[];if(t=[].concat(t),e=[].concat(e),!e.length)return t;if(!t.length)return r?nt.flatten(e).map(a=>`{${a}}`):e;for(let a of t)if(Array.isArray(a))for(let c of a)i.push(qe(c,e,r));else for(let c of e)r===!0&&typeof c=="string"&&(c=`{${c}}`),i.push(Array.isArray(c)?qe(a,c,r):a+c);return nt.flatten(i)},sp=(t,e={})=>{let r=e.rangeLimit===void 0?1e3:e.rangeLimit,i=(a,c={})=>{a.queue=[];let u=c,x=c.queue;for(;u.type!=="brace"&&u.type!=="root"&&u.parent;)u=u.parent,x=u.queue;if(a.invalid||a.dollar){x.push(qe(x.pop(),so(a,e)));return}if(a.type==="brace"&&a.invalid!==!0&&a.nodes.length===2){x.push(qe(x.pop(),["{}"]));return}if(a.nodes&&a.ranges>0){let m=nt.reduce(a.nodes);if(nt.exceedsLimit(...m,e.step,r))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let l=np(...m,e);l.length===0&&(l=so(a,e)),x.push(qe(x.pop(),l)),a.nodes=[];return}let d=nt.encloseBrace(a),p=a.queue,h=a;for(;h.type!=="brace"&&h.type!=="root"&&h.parent;)h=h.parent,p=h.queue;for(let m=0;m{"use strict";uo.exports={MAX_LENGTH:1e4,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` +`,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var mo=_((bx,po)=>{"use strict";var ap=zt(),{MAX_LENGTH:lo,CHAR_BACKSLASH:mi,CHAR_BACKTICK:op,CHAR_COMMA:up,CHAR_DOT:cp,CHAR_LEFT_PARENTHESES:lp,CHAR_RIGHT_PARENTHESES:hp,CHAR_LEFT_CURLY_BRACE:fp,CHAR_RIGHT_CURLY_BRACE:pp,CHAR_LEFT_SQUARE_BRACKET:ho,CHAR_RIGHT_SQUARE_BRACKET:fo,CHAR_DOUBLE_QUOTE:dp,CHAR_SINGLE_QUOTE:mp,CHAR_NO_BREAK_SPACE:xp,CHAR_ZERO_WIDTH_NOBREAK_SPACE:Ep}=co(),gp=(t,e={})=>{if(typeof t!="string")throw new TypeError("Expected a string");let r=e||{},i=typeof r.maxLength=="number"?Math.min(lo,r.maxLength):lo;if(t.length>i)throw new SyntaxError(`Input length (${t.length}), exceeds max characters (${i})`);let a={type:"root",input:t,nodes:[]},c=[a],u=a,x=a,d=0,p=t.length,h=0,m=0,l,n=()=>t[h++],s=o=>{if(o.type==="text"&&x.type==="dot"&&(x.type="text"),x&&x.type==="text"&&o.type==="text"){x.value+=o.value;return}return u.nodes.push(o),o.parent=u,o.prev=x,x=o,o};for(s({type:"bos"});h0){if(u.ranges>0){u.ranges=0;let o=u.nodes.shift();u.nodes=[o,{type:"text",value:ap(u)}]}s({type:"comma",value:l}),u.commas++;continue}if(l===cp&&m>0&&u.commas===0){let o=u.nodes;if(m===0||o.length===0){s({type:"text",value:l});continue}if(x.type==="dot"){if(u.range=[],x.value+=l,x.type="range",u.nodes.length!==3&&u.nodes.length!==5){u.invalid=!0,u.ranges=0,x.type="text";continue}u.ranges++,u.args=[];continue}if(x.type==="range"){o.pop();let f=o[o.length-1];f.value+=x.value+l,x=f,u.ranges--;continue}s({type:"dot",value:l});continue}s({type:"text",value:l})}do if(u=c.pop(),u.type!=="root"){u.nodes.forEach(E=>{E.nodes||(E.type==="open"&&(E.isOpen=!0),E.type==="close"&&(E.isClose=!0),E.nodes||(E.type="text"),E.invalid=!0)});let o=c[c.length-1],f=o.nodes.indexOf(u);o.nodes.splice(f,1,...u.nodes)}while(c.length>0);return s({type:"eos"}),a};po.exports=gp});var go=_((Fx,Eo)=>{"use strict";var xo=zt(),Dp=no(),yp=oo(),vp=mo(),ce=(t,e={})=>{let r=[];if(Array.isArray(t))for(let i of t){let a=ce.create(i,e);Array.isArray(a)?r.push(...a):r.push(a)}else r=[].concat(ce.create(t,e));return e&&e.expand===!0&&e.nodupes===!0&&(r=[...new Set(r)]),r};ce.parse=(t,e={})=>vp(t,e);ce.stringify=(t,e={})=>xo(typeof t=="string"?ce.parse(t,e):t,e);ce.compile=(t,e={})=>(typeof t=="string"&&(t=ce.parse(t,e)),Dp(t,e));ce.expand=(t,e={})=>{typeof t=="string"&&(t=ce.parse(t,e));let r=yp(t,e);return e.noempty===!0&&(r=r.filter(Boolean)),e.nodupes===!0&&(r=[...new Set(r)]),r};ce.create=(t,e={})=>t===""||t.length<3?[t]:e.expand!==!0?ce.compile(t,e):ce.expand(t,e);Eo.exports=ce});var St=_((wx,Ao)=>{"use strict";var Sp=O("path"),Ae="\\\\/",Do=`[^${Ae}]`,ke="\\.",Ap="\\+",Cp="\\?",Kt="\\/",bp="(?=.)",yo="[^/]",xi=`(?:${Kt}|$)`,vo=`(?:^|${Kt})`,Ei=`${ke}{1,2}${xi}`,Fp=`(?!${ke})`,wp=`(?!${vo}${Ei})`,_p=`(?!${ke}{0,1}${xi})`,kp=`(?!${Ei})`,Tp=`[^.${Kt}]`,Bp=`${yo}*?`,So={DOT_LITERAL:ke,PLUS_LITERAL:Ap,QMARK_LITERAL:Cp,SLASH_LITERAL:Kt,ONE_CHAR:bp,QMARK:yo,END_ANCHOR:xi,DOTS_SLASH:Ei,NO_DOT:Fp,NO_DOTS:wp,NO_DOT_SLASH:_p,NO_DOTS_SLASH:kp,QMARK_NO_DOT:Tp,STAR:Bp,START_ANCHOR:vo},Pp={...So,SLASH_LITERAL:`[${Ae}]`,QMARK:Do,STAR:`${Do}*?`,DOTS_SLASH:`${ke}{1,2}(?:[${Ae}]|$)`,NO_DOT:`(?!${ke})`,NO_DOTS:`(?!(?:^|[${Ae}])${ke}{1,2}(?:[${Ae}]|$))`,NO_DOT_SLASH:`(?!${ke}{0,1}(?:[${Ae}]|$))`,NO_DOTS_SLASH:`(?!${ke}{1,2}(?:[${Ae}]|$))`,QMARK_NO_DOT:`[^.${Ae}]`,START_ANCHOR:`(?:^|[${Ae}])`,END_ANCHOR:`(?:[${Ae}]|$)`},Rp={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};Ao.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:Rp,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:Sp.sep,extglobChars(t){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${t.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(t){return t===!0?Pp:So}}});var At=_(se=>{"use strict";var Ip=O("path"),Np=process.platform==="win32",{REGEX_BACKSLASH:Lp,REGEX_REMOVE_BACKSLASH:Op,REGEX_SPECIAL_CHARS:Mp,REGEX_SPECIAL_CHARS_GLOBAL:Hp}=St();se.isObject=t=>t!==null&&typeof t=="object"&&!Array.isArray(t);se.hasRegexChars=t=>Mp.test(t);se.isRegexChar=t=>t.length===1&&se.hasRegexChars(t);se.escapeRegex=t=>t.replace(Hp,"\\$1");se.toPosixSlashes=t=>t.replace(Lp,"/");se.removeBackslashes=t=>t.replace(Op,e=>e==="\\"?"":e);se.supportsLookbehinds=()=>{let t=process.version.slice(1).split(".").map(Number);return t.length===3&&t[0]>=9||t[0]===8&&t[1]>=10};se.isWindows=t=>t&&typeof t.windows=="boolean"?t.windows:Np===!0||Ip.sep==="\\";se.escapeLast=(t,e,r)=>{let i=t.lastIndexOf(e,r);return i===-1?t:t[i-1]==="\\"?se.escapeLast(t,e,i-1):`${t.slice(0,i)}\\${t.slice(i)}`};se.removePrefix=(t,e={})=>{let r=t;return r.startsWith("./")&&(r=r.slice(2),e.prefix="./"),r};se.wrapOutput=(t,e={},r={})=>{let i=r.contains?"":"^",a=r.contains?"":"$",c=`${i}(?:${t})${a}`;return e.negated===!0&&(c=`(?:^(?!${c}).*$)`),c}});var Bo=_((kx,To)=>{"use strict";var Co=At(),{CHAR_ASTERISK:gi,CHAR_AT:Xp,CHAR_BACKWARD_SLASH:Ct,CHAR_COMMA:Up,CHAR_DOT:Di,CHAR_EXCLAMATION_MARK:yi,CHAR_FORWARD_SLASH:ko,CHAR_LEFT_CURLY_BRACE:vi,CHAR_LEFT_PARENTHESES:Si,CHAR_LEFT_SQUARE_BRACKET:$p,CHAR_PLUS:jp,CHAR_QUESTION_MARK:bo,CHAR_RIGHT_CURLY_BRACE:Jp,CHAR_RIGHT_PARENTHESES:Fo,CHAR_RIGHT_SQUARE_BRACKET:zp}=St(),wo=t=>t===ko||t===Ct,_o=t=>{t.isPrefix!==!0&&(t.depth=t.isGlobstar?1/0:1)},qp=(t,e)=>{let r=e||{},i=t.length-1,a=r.parts===!0||r.scanToEnd===!0,c=[],u=[],x=[],d=t,p=-1,h=0,m=0,l=!1,n=!1,s=!1,o=!1,f=!1,E=!1,g=!1,v=!1,F=!1,A=!1,T=0,k,P,N={value:"",depth:0,isGlob:!1},U=()=>p>=i,S=()=>d.charCodeAt(p+1),j=()=>(k=P,d.charCodeAt(++p));for(;p0&&(ye=d.slice(0,h),d=d.slice(h),m-=h),J&&s===!0&&m>0?(J=d.slice(0,m),C=d.slice(m)):s===!0?(J="",C=d):J=d,J&&J!==""&&J!=="/"&&J!==d&&wo(J.charCodeAt(J.length-1))&&(J=J.slice(0,-1)),r.unescape===!0&&(C&&(C=Co.removeBackslashes(C)),J&&g===!0&&(J=Co.removeBackslashes(J)));let b={prefix:ye,input:t,start:h,base:J,glob:C,isBrace:l,isBracket:n,isGlob:s,isExtglob:o,isGlobstar:f,negated:v,negatedExtglob:F};if(r.tokens===!0&&(b.maxDepth=0,wo(P)||u.push(N),b.tokens=u),r.parts===!0||r.tokens===!0){let Y;for(let $=0;${"use strict";var Wt=St(),le=At(),{MAX_LENGTH:Vt,POSIX_REGEX_SOURCE:Gp,REGEX_NON_SPECIAL_CHARS:Kp,REGEX_SPECIAL_CHARS_BACKREF:Wp,REPLACEMENTS:Po}=Wt,Vp=(t,e)=>{if(typeof e.expandRange=="function")return e.expandRange(...t,e);t.sort();let r=`[${t.join("-")}]`;try{new RegExp(r)}catch{return t.map(a=>le.escapeRegex(a)).join("..")}return r},st=(t,e)=>`Missing ${t}: "${e}" - use "\\\\${e}" to match literal characters`,Ai=(t,e)=>{if(typeof t!="string")throw new TypeError("Expected a string");t=Po[t]||t;let r={...e},i=typeof r.maxLength=="number"?Math.min(Vt,r.maxLength):Vt,a=t.length;if(a>i)throw new SyntaxError(`Input length: ${a}, exceeds maximum allowed length: ${i}`);let c={type:"bos",value:"",output:r.prepend||""},u=[c],x=r.capture?"":"?:",d=le.isWindows(e),p=Wt.globChars(d),h=Wt.extglobChars(p),{DOT_LITERAL:m,PLUS_LITERAL:l,SLASH_LITERAL:n,ONE_CHAR:s,DOTS_SLASH:o,NO_DOT:f,NO_DOT_SLASH:E,NO_DOTS_SLASH:g,QMARK:v,QMARK_NO_DOT:F,STAR:A,START_ANCHOR:T}=p,k=R=>`(${x}(?:(?!${T}${R.dot?o:m}).)*?)`,P=r.dot?"":f,N=r.dot?v:F,U=r.bash===!0?k(r):A;r.capture&&(U=`(${U})`),typeof r.noext=="boolean"&&(r.noextglob=r.noext);let S={input:t,index:-1,start:0,dot:r.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:u};t=le.removePrefix(t,S),a=t.length;let j=[],J=[],ye=[],C=c,b,Y=()=>S.index===a-1,$=S.peek=(R=1)=>t[S.index+R],he=S.advance=()=>t[++S.index]||"",fe=()=>t.slice(S.index+1),ie=(R="",z=0)=>{S.consumed+=R,S.index+=z},Ye=R=>{S.output+=R.output!=null?R.output:R.value,ie(R.value)},Ar=()=>{let R=1;for(;$()==="!"&&($(2)!=="("||$(3)==="?");)he(),S.start++,R++;return R%2===0?!1:(S.negated=!0,S.start++,!0)},Qe=R=>{S[R]++,ye.push(R)},we=R=>{S[R]--,ye.pop()},X=R=>{if(C.type==="globstar"){let z=S.braces>0&&(R.type==="comma"||R.type==="brace"),B=R.extglob===!0||j.length&&(R.type==="pipe"||R.type==="paren");R.type!=="slash"&&R.type!=="paren"&&!z&&!B&&(S.output=S.output.slice(0,-C.output.length),C.type="star",C.value="*",C.output=U,S.output+=C.output)}if(j.length&&R.type!=="paren"&&(j[j.length-1].inner+=R.value),(R.value||R.output)&&Ye(R),C&&C.type==="text"&&R.type==="text"){C.value+=R.value,C.output=(C.output||"")+R.value;return}R.prev=C,u.push(R),C=R},Ze=(R,z)=>{let B={...h[z],conditions:1,inner:""};B.prev=C,B.parens=S.parens,B.output=S.output;let H=(r.capture?"(":"")+B.open;Qe("parens"),X({type:R,value:z,output:S.output?"":s}),X({type:"paren",extglob:!0,value:he(),output:H}),j.push(B)},Cr=R=>{let z=R.close+(r.capture?")":""),B;if(R.type==="negate"){let H=U;if(R.inner&&R.inner.length>1&&R.inner.includes("/")&&(H=k(r)),(H!==U||Y()||/^\)+$/.test(fe()))&&(z=R.close=`)$))${H}`),R.inner.includes("*")&&(B=fe())&&/^\.[^\\/.]+$/.test(B)){let q=Ai(B,{...e,fastpaths:!1}).output;z=R.close=`)${q})${H})`}R.prev.type==="bos"&&(S.negatedExtglob=!0)}X({type:"paren",extglob:!0,value:b,output:z}),we("parens")};if(r.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(t)){let R=!1,z=t.replace(Wp,(B,H,q,Q,K,pt)=>Q==="\\"?(R=!0,B):Q==="?"?H?H+Q+(K?v.repeat(K.length):""):pt===0?N+(K?v.repeat(K.length):""):v.repeat(q.length):Q==="."?m.repeat(q.length):Q==="*"?H?H+Q+(K?U:""):U:H?B:`\\${B}`);return R===!0&&(r.unescape===!0?z=z.replace(/\\/g,""):z=z.replace(/\\+/g,B=>B.length%2===0?"\\\\":B?"\\":"")),z===t&&r.contains===!0?(S.output=t,S):(S.output=le.wrapOutput(z,S,e),S)}for(;!Y();){if(b=he(),b==="\0")continue;if(b==="\\"){let B=$();if(B==="/"&&r.bash!==!0||B==="."||B===";")continue;if(!B){b+="\\",X({type:"text",value:b});continue}let H=/^\\+/.exec(fe()),q=0;if(H&&H[0].length>2&&(q=H[0].length,S.index+=q,q%2!==0&&(b+="\\")),r.unescape===!0?b=he():b+=he(),S.brackets===0){X({type:"text",value:b});continue}}if(S.brackets>0&&(b!=="]"||C.value==="["||C.value==="[^")){if(r.posix!==!1&&b===":"){let B=C.value.slice(1);if(B.includes("[")&&(C.posix=!0,B.includes(":"))){let H=C.value.lastIndexOf("["),q=C.value.slice(0,H),Q=C.value.slice(H+2),K=Gp[Q];if(K){C.value=q+K,S.backtrack=!0,he(),!c.output&&u.indexOf(C)===1&&(c.output=s);continue}}}(b==="["&&$()!==":"||b==="-"&&$()==="]")&&(b=`\\${b}`),b==="]"&&(C.value==="["||C.value==="[^")&&(b=`\\${b}`),r.posix===!0&&b==="!"&&C.value==="["&&(b="^"),C.value+=b,Ye({value:b});continue}if(S.quotes===1&&b!=='"'){b=le.escapeRegex(b),C.value+=b,Ye({value:b});continue}if(b==='"'){S.quotes=S.quotes===1?0:1,r.keepQuotes===!0&&X({type:"text",value:b});continue}if(b==="("){Qe("parens"),X({type:"paren",value:b});continue}if(b===")"){if(S.parens===0&&r.strictBrackets===!0)throw new SyntaxError(st("opening","("));let B=j[j.length-1];if(B&&S.parens===B.parens+1){Cr(j.pop());continue}X({type:"paren",value:b,output:S.parens?")":"\\)"}),we("parens");continue}if(b==="["){if(r.nobracket===!0||!fe().includes("]")){if(r.nobracket!==!0&&r.strictBrackets===!0)throw new SyntaxError(st("closing","]"));b=`\\${b}`}else Qe("brackets");X({type:"bracket",value:b});continue}if(b==="]"){if(r.nobracket===!0||C&&C.type==="bracket"&&C.value.length===1){X({type:"text",value:b,output:`\\${b}`});continue}if(S.brackets===0){if(r.strictBrackets===!0)throw new SyntaxError(st("opening","["));X({type:"text",value:b,output:`\\${b}`});continue}we("brackets");let B=C.value.slice(1);if(C.posix!==!0&&B[0]==="^"&&!B.includes("/")&&(b=`/${b}`),C.value+=b,Ye({value:b}),r.literalBrackets===!1||le.hasRegexChars(B))continue;let H=le.escapeRegex(C.value);if(S.output=S.output.slice(0,-C.value.length),r.literalBrackets===!0){S.output+=H,C.value=H;continue}C.value=`(${x}${H}|${C.value})`,S.output+=C.value;continue}if(b==="{"&&r.nobrace!==!0){Qe("braces");let B={type:"brace",value:b,output:"(",outputIndex:S.output.length,tokensIndex:S.tokens.length};J.push(B),X(B);continue}if(b==="}"){let B=J[J.length-1];if(r.nobrace===!0||!B){X({type:"text",value:b,output:b});continue}let H=")";if(B.dots===!0){let q=u.slice(),Q=[];for(let K=q.length-1;K>=0&&(u.pop(),q[K].type!=="brace");K--)q[K].type!=="dots"&&Q.unshift(q[K].value);H=Vp(Q,r),S.backtrack=!0}if(B.comma!==!0&&B.dots!==!0){let q=S.output.slice(0,B.outputIndex),Q=S.tokens.slice(B.tokensIndex);B.value=B.output="\\{",b=H="\\}",S.output=q;for(let K of Q)S.output+=K.output||K.value}X({type:"brace",value:b,output:H}),we("braces"),J.pop();continue}if(b==="|"){j.length>0&&j[j.length-1].conditions++,X({type:"text",value:b});continue}if(b===","){let B=b,H=J[J.length-1];H&&ye[ye.length-1]==="braces"&&(H.comma=!0,B="|"),X({type:"comma",value:b,output:B});continue}if(b==="/"){if(C.type==="dot"&&S.index===S.start+1){S.start=S.index+1,S.consumed="",S.output="",u.pop(),C=c;continue}X({type:"slash",value:b,output:n});continue}if(b==="."){if(S.braces>0&&C.type==="dot"){C.value==="."&&(C.output=m);let B=J[J.length-1];C.type="dots",C.output+=b,C.value+=b,B.dots=!0;continue}if(S.braces+S.parens===0&&C.type!=="bos"&&C.type!=="slash"){X({type:"text",value:b,output:m});continue}X({type:"dot",value:b,output:m});continue}if(b==="?"){if(!(C&&C.value==="(")&&r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Ze("qmark",b);continue}if(C&&C.type==="paren"){let H=$(),q=b;if(H==="<"&&!le.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(C.value==="("&&!/[!=<:]/.test(H)||H==="<"&&!/<([!=]|\w+>)/.test(fe()))&&(q=`\\${b}`),X({type:"text",value:b,output:q});continue}if(r.dot!==!0&&(C.type==="slash"||C.type==="bos")){X({type:"qmark",value:b,output:F});continue}X({type:"qmark",value:b,output:v});continue}if(b==="!"){if(r.noextglob!==!0&&$()==="("&&($(2)!=="?"||!/[!=<:]/.test($(3)))){Ze("negate",b);continue}if(r.nonegate!==!0&&S.index===0){Ar();continue}}if(b==="+"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Ze("plus",b);continue}if(C&&C.value==="("||r.regex===!1){X({type:"plus",value:b,output:l});continue}if(C&&(C.type==="bracket"||C.type==="paren"||C.type==="brace")||S.parens>0){X({type:"plus",value:b});continue}X({type:"plus",value:l});continue}if(b==="@"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){X({type:"at",extglob:!0,value:b,output:""});continue}X({type:"text",value:b});continue}if(b!=="*"){(b==="$"||b==="^")&&(b=`\\${b}`);let B=Kp.exec(fe());B&&(b+=B[0],S.index+=B[0].length),X({type:"text",value:b});continue}if(C&&(C.type==="globstar"||C.star===!0)){C.type="star",C.star=!0,C.value+=b,C.output=U,S.backtrack=!0,S.globstar=!0,ie(b);continue}let R=fe();if(r.noextglob!==!0&&/^\([^?]/.test(R)){Ze("star",b);continue}if(C.type==="star"){if(r.noglobstar===!0){ie(b);continue}let B=C.prev,H=B.prev,q=B.type==="slash"||B.type==="bos",Q=H&&(H.type==="star"||H.type==="globstar");if(r.bash===!0&&(!q||R[0]&&R[0]!=="/")){X({type:"star",value:b,output:""});continue}let K=S.braces>0&&(B.type==="comma"||B.type==="brace"),pt=j.length&&(B.type==="pipe"||B.type==="paren");if(!q&&B.type!=="paren"&&!K&&!pt){X({type:"star",value:b,output:""});continue}for(;R.slice(0,3)==="/**";){let et=t[S.index+4];if(et&&et!=="/")break;R=R.slice(3),ie("/**",3)}if(B.type==="bos"&&Y()){C.type="globstar",C.value+=b,C.output=k(r),S.output=C.output,S.globstar=!0,ie(b);continue}if(B.type==="slash"&&B.prev.type!=="bos"&&!Q&&Y()){S.output=S.output.slice(0,-(B.output+C.output).length),B.output=`(?:${B.output}`,C.type="globstar",C.output=k(r)+(r.strictSlashes?")":"|$)"),C.value+=b,S.globstar=!0,S.output+=B.output+C.output,ie(b);continue}if(B.type==="slash"&&B.prev.type!=="bos"&&R[0]==="/"){let et=R[1]!==void 0?"|$":"";S.output=S.output.slice(0,-(B.output+C.output).length),B.output=`(?:${B.output}`,C.type="globstar",C.output=`${k(r)}${n}|${n}${et})`,C.value+=b,S.output+=B.output+C.output,S.globstar=!0,ie(b+he()),X({type:"slash",value:"/",output:""});continue}if(B.type==="bos"&&R[0]==="/"){C.type="globstar",C.value+=b,C.output=`(?:^|${n}|${k(r)}${n})`,S.output=C.output,S.globstar=!0,ie(b+he()),X({type:"slash",value:"/",output:""});continue}S.output=S.output.slice(0,-C.output.length),C.type="globstar",C.output=k(r),C.value+=b,S.output+=C.output,S.globstar=!0,ie(b);continue}let z={type:"star",value:b,output:U};if(r.bash===!0){z.output=".*?",(C.type==="bos"||C.type==="slash")&&(z.output=P+z.output),X(z);continue}if(C&&(C.type==="bracket"||C.type==="paren")&&r.regex===!0){z.output=b,X(z);continue}(S.index===S.start||C.type==="slash"||C.type==="dot")&&(C.type==="dot"?(S.output+=E,C.output+=E):r.dot===!0?(S.output+=g,C.output+=g):(S.output+=P,C.output+=P),$()!=="*"&&(S.output+=s,C.output+=s)),X(z)}for(;S.brackets>0;){if(r.strictBrackets===!0)throw new SyntaxError(st("closing","]"));S.output=le.escapeLast(S.output,"["),we("brackets")}for(;S.parens>0;){if(r.strictBrackets===!0)throw new SyntaxError(st("closing",")"));S.output=le.escapeLast(S.output,"("),we("parens")}for(;S.braces>0;){if(r.strictBrackets===!0)throw new SyntaxError(st("closing","}"));S.output=le.escapeLast(S.output,"{"),we("braces")}if(r.strictSlashes!==!0&&(C.type==="star"||C.type==="bracket")&&X({type:"maybe_slash",value:"",output:`${n}?`}),S.backtrack===!0){S.output="";for(let R of S.tokens)S.output+=R.output!=null?R.output:R.value,R.suffix&&(S.output+=R.suffix)}return S};Ai.fastpaths=(t,e)=>{let r={...e},i=typeof r.maxLength=="number"?Math.min(Vt,r.maxLength):Vt,a=t.length;if(a>i)throw new SyntaxError(`Input length: ${a}, exceeds maximum allowed length: ${i}`);t=Po[t]||t;let c=le.isWindows(e),{DOT_LITERAL:u,SLASH_LITERAL:x,ONE_CHAR:d,DOTS_SLASH:p,NO_DOT:h,NO_DOTS:m,NO_DOTS_SLASH:l,STAR:n,START_ANCHOR:s}=Wt.globChars(c),o=r.dot?m:h,f=r.dot?l:h,E=r.capture?"":"?:",g={negated:!1,prefix:""},v=r.bash===!0?".*?":n;r.capture&&(v=`(${v})`);let F=P=>P.noglobstar===!0?v:`(${E}(?:(?!${s}${P.dot?p:u}).)*?)`,A=P=>{switch(P){case"*":return`${o}${d}${v}`;case".*":return`${u}${d}${v}`;case"*.*":return`${o}${v}${u}${d}${v}`;case"*/*":return`${o}${v}${x}${d}${f}${v}`;case"**":return o+F(r);case"**/*":return`(?:${o}${F(r)}${x})?${f}${d}${v}`;case"**/*.*":return`(?:${o}${F(r)}${x})?${f}${v}${u}${d}${v}`;case"**/.*":return`(?:${o}${F(r)}${x})?${u}${d}${v}`;default:{let N=/^(.*?)\.(\w+)$/.exec(P);if(!N)return;let U=A(N[1]);return U?U+u+N[2]:void 0}}},T=le.removePrefix(t,g),k=A(T);return k&&r.strictSlashes!==!0&&(k+=`${x}?`),k};Ro.exports=Ai});var Lo=_((Bx,No)=>{"use strict";var Yp=O("path"),Qp=Bo(),Ci=Io(),bi=At(),Zp=St(),ed=t=>t&&typeof t=="object"&&!Array.isArray(t),W=(t,e,r=!1)=>{if(Array.isArray(t)){let h=t.map(l=>W(l,e,r));return l=>{for(let n of h){let s=n(l);if(s)return s}return!1}}let i=ed(t)&&t.tokens&&t.input;if(t===""||typeof t!="string"&&!i)throw new TypeError("Expected pattern to be a non-empty string");let a=e||{},c=bi.isWindows(e),u=i?W.compileRe(t,e):W.makeRe(t,e,!1,!0),x=u.state;delete u.state;let d=()=>!1;if(a.ignore){let h={...e,ignore:null,onMatch:null,onResult:null};d=W(a.ignore,h,r)}let p=(h,m=!1)=>{let{isMatch:l,match:n,output:s}=W.test(h,u,e,{glob:t,posix:c}),o={glob:t,state:x,regex:u,posix:c,input:h,output:s,match:n,isMatch:l};return typeof a.onResult=="function"&&a.onResult(o),l===!1?(o.isMatch=!1,m?o:!1):d(h)?(typeof a.onIgnore=="function"&&a.onIgnore(o),o.isMatch=!1,m?o:!1):(typeof a.onMatch=="function"&&a.onMatch(o),m?o:!0)};return r&&(p.state=x),p};W.test=(t,e,r,{glob:i,posix:a}={})=>{if(typeof t!="string")throw new TypeError("Expected input to be a string");if(t==="")return{isMatch:!1,output:""};let c=r||{},u=c.format||(a?bi.toPosixSlashes:null),x=t===i,d=x&&u?u(t):t;return x===!1&&(d=u?u(t):t,x=d===i),(x===!1||c.capture===!0)&&(c.matchBase===!0||c.basename===!0?x=W.matchBase(t,e,r,a):x=e.exec(d)),{isMatch:!!x,match:x,output:d}};W.matchBase=(t,e,r,i=bi.isWindows(r))=>(e instanceof RegExp?e:W.makeRe(e,r)).test(Yp.basename(t));W.isMatch=(t,e,r)=>W(e,r)(t);W.parse=(t,e)=>Array.isArray(t)?t.map(r=>W.parse(r,e)):Ci(t,{...e,fastpaths:!1});W.scan=(t,e)=>Qp(t,e);W.compileRe=(t,e,r=!1,i=!1)=>{if(r===!0)return t.output;let a=e||{},c=a.contains?"":"^",u=a.contains?"":"$",x=`${c}(?:${t.output})${u}`;t&&t.negated===!0&&(x=`^(?!${x}).*$`);let d=W.toRegex(x,e);return i===!0&&(d.state=t),d};W.makeRe=(t,e={},r=!1,i=!1)=>{if(!t||typeof t!="string")throw new TypeError("Expected a non-empty string");let a={negated:!1,fastpaths:!0};return e.fastpaths!==!1&&(t[0]==="."||t[0]==="*")&&(a.output=Ci.fastpaths(t,e)),a.output||(a=Ci(t,e)),W.compileRe(a,e,r,i)};W.toRegex=(t,e)=>{try{let r=e||{};return new RegExp(t,r.flags||(r.nocase?"i":""))}catch(r){if(e&&e.debug===!0)throw r;return/$^/}};W.constants=Zp;No.exports=W});var Mo=_((Px,Oo)=>{"use strict";Oo.exports=Lo()});var Jo=_((Rx,jo)=>{"use strict";var Xo=O("util"),Uo=go(),Ce=Mo(),Fi=At(),Ho=t=>t===""||t==="./",$o=t=>{let e=t.indexOf("{");return e>-1&&t.indexOf("}",e)>-1},G=(t,e,r)=>{e=[].concat(e),t=[].concat(t);let i=new Set,a=new Set,c=new Set,u=0,x=h=>{c.add(h.output),r&&r.onResult&&r.onResult(h)};for(let h=0;h!i.has(h));if(r&&p.length===0){if(r.failglob===!0)throw new Error(`No matches found for "${e.join(", ")}"`);if(r.nonull===!0||r.nullglob===!0)return r.unescape?e.map(h=>h.replace(/\\/g,"")):e}return p};G.match=G;G.matcher=(t,e)=>Ce(t,e);G.isMatch=(t,e,r)=>Ce(e,r)(t);G.any=G.isMatch;G.not=(t,e,r={})=>{e=[].concat(e).map(String);let i=new Set,a=[],c=x=>{r.onResult&&r.onResult(x),a.push(x.output)},u=new Set(G(t,e,{...r,onResult:c}));for(let x of a)u.has(x)||i.add(x);return[...i]};G.contains=(t,e,r)=>{if(typeof t!="string")throw new TypeError(`Expected a string: "${Xo.inspect(t)}"`);if(Array.isArray(e))return e.some(i=>G.contains(t,i,r));if(typeof e=="string"){if(Ho(t)||Ho(e))return!1;if(t.includes(e)||t.startsWith("./")&&t.slice(2).includes(e))return!0}return G.isMatch(t,e,{...r,contains:!0})};G.matchKeys=(t,e,r)=>{if(!Fi.isObject(t))throw new TypeError("Expected the first argument to be an object");let i=G(Object.keys(t),e,r),a={};for(let c of i)a[c]=t[c];return a};G.some=(t,e,r)=>{let i=[].concat(t);for(let a of[].concat(e)){let c=Ce(String(a),r);if(i.some(u=>c(u)))return!0}return!1};G.every=(t,e,r)=>{let i=[].concat(t);for(let a of[].concat(e)){let c=Ce(String(a),r);if(!i.every(u=>c(u)))return!1}return!0};G.all=(t,e,r)=>{if(typeof t!="string")throw new TypeError(`Expected a string: "${Xo.inspect(t)}"`);return[].concat(e).every(i=>Ce(i,r)(t))};G.capture=(t,e,r)=>{let i=Fi.isWindows(r),c=Ce.makeRe(String(t),{...r,capture:!0}).exec(i?Fi.toPosixSlashes(e):e);if(c)return c.slice(1).map(u=>u===void 0?"":u)};G.makeRe=(...t)=>Ce.makeRe(...t);G.scan=(...t)=>Ce.scan(...t);G.parse=(t,e)=>{let r=[];for(let i of[].concat(t||[]))for(let a of Uo(String(i),e))r.push(Ce.parse(a,e));return r};G.braces=(t,e)=>{if(typeof t!="string")throw new TypeError("Expected a string");return e&&e.nobrace===!0||!$o(t)?[t]:Uo(t,e)};G.braceExpand=(t,e)=>{if(typeof t!="string")throw new TypeError("Expected a string");return G.braces(t,{...e,expand:!0})};G.hasBraces=$o;jo.exports=G});var Qo=_(L=>{"use strict";Object.defineProperty(L,"__esModule",{value:!0});L.removeDuplicateSlashes=L.matchAny=L.convertPatternsToRe=L.makeRe=L.getPatternParts=L.expandBraceExpansion=L.expandPatternsWithBraceExpansion=L.isAffectDepthOfReadingPattern=L.endsWithSlashGlobStar=L.hasGlobStar=L.getBaseDirectory=L.isPatternRelatedToParentDirectory=L.getPatternsOutsideCurrentDirectory=L.getPatternsInsideCurrentDirectory=L.getPositivePatterns=L.getNegativePatterns=L.isPositivePattern=L.isNegativePattern=L.convertToNegativePattern=L.convertToPositivePattern=L.isDynamicPattern=L.isStaticPattern=void 0;var td=O("path"),rd=Na(),wi=Jo(),zo="**",id="\\",nd=/[*?]|^!/,sd=/\[[^[]*]/,ad=/(?:^|[^!*+?@])\([^(]*\|[^|]*\)/,od=/[!*+?@]\([^(]*\)/,ud=/,|\.\./,cd=/(?!^)\/{2,}/g;function qo(t,e={}){return!Go(t,e)}L.isStaticPattern=qo;function Go(t,e={}){return t===""?!1:!!(e.caseSensitiveMatch===!1||t.includes(id)||nd.test(t)||sd.test(t)||ad.test(t)||e.extglob!==!1&&od.test(t)||e.braceExpansion!==!1&&ld(t))}L.isDynamicPattern=Go;function ld(t){let e=t.indexOf("{");if(e===-1)return!1;let r=t.indexOf("}",e+1);if(r===-1)return!1;let i=t.slice(e,r);return ud.test(i)}function hd(t){return Yt(t)?t.slice(1):t}L.convertToPositivePattern=hd;function fd(t){return"!"+t}L.convertToNegativePattern=fd;function Yt(t){return t.startsWith("!")&&t[1]!=="("}L.isNegativePattern=Yt;function Ko(t){return!Yt(t)}L.isPositivePattern=Ko;function pd(t){return t.filter(Yt)}L.getNegativePatterns=pd;function dd(t){return t.filter(Ko)}L.getPositivePatterns=dd;function md(t){return t.filter(e=>!_i(e))}L.getPatternsInsideCurrentDirectory=md;function xd(t){return t.filter(_i)}L.getPatternsOutsideCurrentDirectory=xd;function _i(t){return t.startsWith("..")||t.startsWith("./..")}L.isPatternRelatedToParentDirectory=_i;function Ed(t){return rd(t,{flipBackslashes:!1})}L.getBaseDirectory=Ed;function gd(t){return t.includes(zo)}L.hasGlobStar=gd;function Wo(t){return t.endsWith("/"+zo)}L.endsWithSlashGlobStar=Wo;function Dd(t){let e=td.basename(t);return Wo(t)||qo(e)}L.isAffectDepthOfReadingPattern=Dd;function yd(t){return t.reduce((e,r)=>e.concat(Vo(r)),[])}L.expandPatternsWithBraceExpansion=yd;function Vo(t){let e=wi.braces(t,{expand:!0,nodupes:!0,keepEscaping:!0});return e.sort((r,i)=>r.length-i.length),e.filter(r=>r!=="")}L.expandBraceExpansion=Vo;function vd(t,e){let{parts:r}=wi.scan(t,Object.assign(Object.assign({},e),{parts:!0}));return r.length===0&&(r=[t]),r[0].startsWith("/")&&(r[0]=r[0].slice(1),r.unshift("")),r}L.getPatternParts=vd;function Yo(t,e){return wi.makeRe(t,e)}L.makeRe=Yo;function Sd(t,e){return t.map(r=>Yo(r,e))}L.convertPatternsToRe=Sd;function Ad(t,e){return e.some(r=>r.test(t))}L.matchAny=Ad;function Cd(t){return t.replace(cd,"/")}L.removeDuplicateSlashes=Cd});var ru=_((Nx,tu)=>{"use strict";var bd=O("stream"),Zo=bd.PassThrough,Fd=Array.prototype.slice;tu.exports=wd;function wd(){let t=[],e=Fd.call(arguments),r=!1,i=e[e.length-1];i&&!Array.isArray(i)&&i.pipe==null?e.pop():i={};let a=i.end!==!1,c=i.pipeError===!0;i.objectMode==null&&(i.objectMode=!0),i.highWaterMark==null&&(i.highWaterMark=64*1024);let u=Zo(i);function x(){for(let h=0,m=arguments.length;h0||(r=!1,d())}function n(s){function o(){s.removeListener("merge2UnpipeEnd",o),s.removeListener("end",o),c&&s.removeListener("error",f),l()}function f(E){u.emit("error",E)}if(s._readableState.endEmitted)return l();s.on("merge2UnpipeEnd",o),s.on("end",o),c&&s.on("error",f),s.pipe(u,{end:!1}),s.resume()}for(let s=0;s{"use strict";Object.defineProperty(Qt,"__esModule",{value:!0});Qt.merge=void 0;var _d=ru();function kd(t){let e=_d(t);return t.forEach(r=>{r.once("error",i=>e.emit("error",i))}),e.once("close",()=>iu(t)),e.once("end",()=>iu(t)),e}Qt.merge=kd;function iu(t){t.forEach(e=>e.emit("close"))}});var su=_(at=>{"use strict";Object.defineProperty(at,"__esModule",{value:!0});at.isEmpty=at.isString=void 0;function Td(t){return typeof t=="string"}at.isString=Td;function Bd(t){return t===""}at.isEmpty=Bd});var Te=_(te=>{"use strict";Object.defineProperty(te,"__esModule",{value:!0});te.string=te.stream=te.pattern=te.path=te.fs=te.errno=te.array=void 0;var Pd=Sa();te.array=Pd;var Rd=Aa();te.errno=Rd;var Id=Ca();te.fs=Id;var Nd=_a();te.path=Nd;var Ld=Qo();te.pattern=Ld;var Od=nu();te.stream=Od;var Md=su();te.string=Md});var cu=_(re=>{"use strict";Object.defineProperty(re,"__esModule",{value:!0});re.convertPatternGroupToTask=re.convertPatternGroupsToTasks=re.groupPatternsByBaseDirectory=re.getNegativePatternsAsPositive=re.getPositivePatterns=re.convertPatternsToTasks=re.generate=void 0;var ge=Te();function Hd(t,e){let r=au(t,e),i=au(e.ignore,e),a=ou(r),c=uu(r,i),u=a.filter(h=>ge.pattern.isStaticPattern(h,e)),x=a.filter(h=>ge.pattern.isDynamicPattern(h,e)),d=ki(u,c,!1),p=ki(x,c,!0);return d.concat(p)}re.generate=Hd;function au(t,e){let r=t;return e.braceExpansion&&(r=ge.pattern.expandPatternsWithBraceExpansion(r)),e.baseNameMatch&&(r=r.map(i=>i.includes("/")?i:`**/${i}`)),r.map(i=>ge.pattern.removeDuplicateSlashes(i))}function ki(t,e,r){let i=[],a=ge.pattern.getPatternsOutsideCurrentDirectory(t),c=ge.pattern.getPatternsInsideCurrentDirectory(t),u=Ti(a),x=Ti(c);return i.push(...Bi(u,e,r)),"."in x?i.push(Pi(".",c,e,r)):i.push(...Bi(x,e,r)),i}re.convertPatternsToTasks=ki;function ou(t){return ge.pattern.getPositivePatterns(t)}re.getPositivePatterns=ou;function uu(t,e){return ge.pattern.getNegativePatterns(t).concat(e).map(ge.pattern.convertToPositivePattern)}re.getNegativePatternsAsPositive=uu;function Ti(t){let e={};return t.reduce((r,i)=>{let a=ge.pattern.getBaseDirectory(i);return a in r?r[a].push(i):r[a]=[i],r},e)}re.groupPatternsByBaseDirectory=Ti;function Bi(t,e,r){return Object.keys(t).map(i=>Pi(i,t[i],e,r))}re.convertPatternGroupsToTasks=Bi;function Pi(t,e,r,i){return{dynamic:i,positive:e,negative:r,base:t,patterns:[].concat(e,r.map(ge.pattern.convertToNegativePattern))}}re.convertPatternGroupToTask=Pi});var hu=_(Zt=>{"use strict";Object.defineProperty(Zt,"__esModule",{value:!0});Zt.read=void 0;function Xd(t,e,r){e.fs.lstat(t,(i,a)=>{if(i!==null){lu(r,i);return}if(!a.isSymbolicLink()||!e.followSymbolicLink){Ri(r,a);return}e.fs.stat(t,(c,u)=>{if(c!==null){if(e.throwErrorOnBrokenSymbolicLink){lu(r,c);return}Ri(r,a);return}e.markSymbolicLink&&(u.isSymbolicLink=()=>!0),Ri(r,u)})})}Zt.read=Xd;function lu(t,e){t(e)}function Ri(t,e){t(null,e)}});var fu=_(er=>{"use strict";Object.defineProperty(er,"__esModule",{value:!0});er.read=void 0;function Ud(t,e){let r=e.fs.lstatSync(t);if(!r.isSymbolicLink()||!e.followSymbolicLink)return r;try{let i=e.fs.statSync(t);return e.markSymbolicLink&&(i.isSymbolicLink=()=>!0),i}catch(i){if(!e.throwErrorOnBrokenSymbolicLink)return r;throw i}}er.read=Ud});var pu=_(Ne=>{"use strict";Object.defineProperty(Ne,"__esModule",{value:!0});Ne.createFileSystemAdapter=Ne.FILE_SYSTEM_ADAPTER=void 0;var tr=O("fs");Ne.FILE_SYSTEM_ADAPTER={lstat:tr.lstat,stat:tr.stat,lstatSync:tr.lstatSync,statSync:tr.statSync};function $d(t){return t===void 0?Ne.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},Ne.FILE_SYSTEM_ADAPTER),t)}Ne.createFileSystemAdapter=$d});var du=_(Ni=>{"use strict";Object.defineProperty(Ni,"__esModule",{value:!0});var jd=pu(),Ii=class{constructor(e={}){this._options=e,this.followSymbolicLink=this._getValue(this._options.followSymbolicLink,!0),this.fs=jd.createFileSystemAdapter(this._options.fs),this.markSymbolicLink=this._getValue(this._options.markSymbolicLink,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0)}_getValue(e,r){return e??r}};Ni.default=Ii});var Ge=_(Le=>{"use strict";Object.defineProperty(Le,"__esModule",{value:!0});Le.statSync=Le.stat=Le.Settings=void 0;var mu=hu(),Jd=fu(),Li=du();Le.Settings=Li.default;function zd(t,e,r){if(typeof e=="function"){mu.read(t,Oi(),e);return}mu.read(t,Oi(e),r)}Le.stat=zd;function qd(t,e){let r=Oi(e);return Jd.read(t,r)}Le.statSync=qd;function Oi(t={}){return t instanceof Li.default?t:new Li.default(t)}});var gu=_((zx,Eu)=>{var xu;Eu.exports=typeof queueMicrotask=="function"?queueMicrotask.bind(typeof window<"u"?window:global):t=>(xu||(xu=Promise.resolve())).then(t).catch(e=>setTimeout(()=>{throw e},0))});var yu=_((qx,Du)=>{Du.exports=Kd;var Gd=gu();function Kd(t,e){let r,i,a,c=!0;Array.isArray(t)?(r=[],i=t.length):(a=Object.keys(t),r={},i=a.length);function u(d){function p(){e&&e(d,r),e=null}c?Gd(p):p()}function x(d,p,h){r[d]=h,(--i===0||p)&&u(p)}i?a?a.forEach(function(d){t[d](function(p,h){x(d,p,h)})}):t.forEach(function(d,p){d(function(h,m){x(p,h,m)})}):u(null),c=!1}});var Mi=_(ir=>{"use strict";Object.defineProperty(ir,"__esModule",{value:!0});ir.IS_SUPPORT_READDIR_WITH_FILE_TYPES=void 0;var rr=process.versions.node.split(".");if(rr[0]===void 0||rr[1]===void 0)throw new Error(`Unexpected behavior. The 'process.versions.node' variable has invalid value: ${process.versions.node}`);var vu=Number.parseInt(rr[0],10),Wd=Number.parseInt(rr[1],10),Su=10,Vd=10,Yd=vu>Su,Qd=vu===Su&&Wd>=Vd;ir.IS_SUPPORT_READDIR_WITH_FILE_TYPES=Yd||Qd});var Au=_(nr=>{"use strict";Object.defineProperty(nr,"__esModule",{value:!0});nr.createDirentFromStats=void 0;var Hi=class{constructor(e,r){this.name=e,this.isBlockDevice=r.isBlockDevice.bind(r),this.isCharacterDevice=r.isCharacterDevice.bind(r),this.isDirectory=r.isDirectory.bind(r),this.isFIFO=r.isFIFO.bind(r),this.isFile=r.isFile.bind(r),this.isSocket=r.isSocket.bind(r),this.isSymbolicLink=r.isSymbolicLink.bind(r)}};function Zd(t,e){return new Hi(t,e)}nr.createDirentFromStats=Zd});var Xi=_(sr=>{"use strict";Object.defineProperty(sr,"__esModule",{value:!0});sr.fs=void 0;var em=Au();sr.fs=em});var Ui=_(ar=>{"use strict";Object.defineProperty(ar,"__esModule",{value:!0});ar.joinPathSegments=void 0;function tm(t,e,r){return t.endsWith(r)?t+e:t+r+e}ar.joinPathSegments=tm});var ku=_(Oe=>{"use strict";Object.defineProperty(Oe,"__esModule",{value:!0});Oe.readdir=Oe.readdirWithFileTypes=Oe.read=void 0;var rm=Ge(),Cu=yu(),im=Mi(),bu=Xi(),Fu=Ui();function nm(t,e,r){if(!e.stats&&im.IS_SUPPORT_READDIR_WITH_FILE_TYPES){wu(t,e,r);return}_u(t,e,r)}Oe.read=nm;function wu(t,e,r){e.fs.readdir(t,{withFileTypes:!0},(i,a)=>{if(i!==null){or(r,i);return}let c=a.map(x=>({dirent:x,name:x.name,path:Fu.joinPathSegments(t,x.name,e.pathSegmentSeparator)}));if(!e.followSymbolicLinks){$i(r,c);return}let u=c.map(x=>sm(x,e));Cu(u,(x,d)=>{if(x!==null){or(r,x);return}$i(r,d)})})}Oe.readdirWithFileTypes=wu;function sm(t,e){return r=>{if(!t.dirent.isSymbolicLink()){r(null,t);return}e.fs.stat(t.path,(i,a)=>{if(i!==null){if(e.throwErrorOnBrokenSymbolicLink){r(i);return}r(null,t);return}t.dirent=bu.fs.createDirentFromStats(t.name,a),r(null,t)})}}function _u(t,e,r){e.fs.readdir(t,(i,a)=>{if(i!==null){or(r,i);return}let c=a.map(u=>{let x=Fu.joinPathSegments(t,u,e.pathSegmentSeparator);return d=>{rm.stat(x,e.fsStatSettings,(p,h)=>{if(p!==null){d(p);return}let m={name:u,path:x,dirent:bu.fs.createDirentFromStats(u,h)};e.stats&&(m.stats=h),d(null,m)})}});Cu(c,(u,x)=>{if(u!==null){or(r,u);return}$i(r,x)})})}Oe.readdir=_u;function or(t,e){t(e)}function $i(t,e){t(null,e)}});var Iu=_(Me=>{"use strict";Object.defineProperty(Me,"__esModule",{value:!0});Me.readdir=Me.readdirWithFileTypes=Me.read=void 0;var am=Ge(),om=Mi(),Tu=Xi(),Bu=Ui();function um(t,e){return!e.stats&&om.IS_SUPPORT_READDIR_WITH_FILE_TYPES?Pu(t,e):Ru(t,e)}Me.read=um;function Pu(t,e){return e.fs.readdirSync(t,{withFileTypes:!0}).map(i=>{let a={dirent:i,name:i.name,path:Bu.joinPathSegments(t,i.name,e.pathSegmentSeparator)};if(a.dirent.isSymbolicLink()&&e.followSymbolicLinks)try{let c=e.fs.statSync(a.path);a.dirent=Tu.fs.createDirentFromStats(a.name,c)}catch(c){if(e.throwErrorOnBrokenSymbolicLink)throw c}return a})}Me.readdirWithFileTypes=Pu;function Ru(t,e){return e.fs.readdirSync(t).map(i=>{let a=Bu.joinPathSegments(t,i,e.pathSegmentSeparator),c=am.statSync(a,e.fsStatSettings),u={name:i,path:a,dirent:Tu.fs.createDirentFromStats(i,c)};return e.stats&&(u.stats=c),u})}Me.readdir=Ru});var Nu=_(He=>{"use strict";Object.defineProperty(He,"__esModule",{value:!0});He.createFileSystemAdapter=He.FILE_SYSTEM_ADAPTER=void 0;var ot=O("fs");He.FILE_SYSTEM_ADAPTER={lstat:ot.lstat,stat:ot.stat,lstatSync:ot.lstatSync,statSync:ot.statSync,readdir:ot.readdir,readdirSync:ot.readdirSync};function cm(t){return t===void 0?He.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},He.FILE_SYSTEM_ADAPTER),t)}He.createFileSystemAdapter=cm});var Lu=_(Ji=>{"use strict";Object.defineProperty(Ji,"__esModule",{value:!0});var lm=O("path"),hm=Ge(),fm=Nu(),ji=class{constructor(e={}){this._options=e,this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!1),this.fs=fm.createFileSystemAdapter(this._options.fs),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,lm.sep),this.stats=this._getValue(this._options.stats,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0),this.fsStatSettings=new hm.Settings({followSymbolicLink:this.followSymbolicLinks,fs:this.fs,throwErrorOnBrokenSymbolicLink:this.throwErrorOnBrokenSymbolicLink})}_getValue(e,r){return e??r}};Ji.default=ji});var ur=_(Xe=>{"use strict";Object.defineProperty(Xe,"__esModule",{value:!0});Xe.Settings=Xe.scandirSync=Xe.scandir=void 0;var Ou=ku(),pm=Iu(),zi=Lu();Xe.Settings=zi.default;function dm(t,e,r){if(typeof e=="function"){Ou.read(t,qi(),e);return}Ou.read(t,qi(e),r)}Xe.scandir=dm;function mm(t,e){let r=qi(e);return pm.read(t,r)}Xe.scandirSync=mm;function qi(t={}){return t instanceof zi.default?t:new zi.default(t)}});var Hu=_((rE,Mu)=>{"use strict";function xm(t){var e=new t,r=e;function i(){var c=e;return c.next?e=c.next:(e=new t,r=e),c.next=null,c}function a(c){r.next=c,r=c}return{get:i,release:a}}Mu.exports=xm});var Uu=_((iE,Gi)=>{"use strict";var Em=Hu();function Xu(t,e,r){if(typeof t=="function"&&(r=e,e=t,t=null),!(r>=1))throw new Error("fastqueue concurrency must be equal to or greater than 1");var i=Em(gm),a=null,c=null,u=0,x=null,d={push:o,drain:me,saturated:me,pause:h,paused:!1,get concurrency(){return r},set concurrency(A){if(!(A>=1))throw new Error("fastqueue concurrency must be equal to or greater than 1");if(r=A,!d.paused)for(;a&&u=r||d.paused?c?(c.next=k,c=k):(a=k,c=k,d.saturated()):(u++,e.call(t,k.value,k.worked))}function f(A,T){var k=i.get();k.context=t,k.release=E,k.value=A,k.callback=T||me,k.errorHandler=x,u>=r||d.paused?a?(k.next=a,a=k):(a=k,c=k,d.saturated()):(u++,e.call(t,k.value,k.worked))}function E(A){A&&i.release(A);var T=a;T&&u<=r?d.paused?u--:(c===a&&(c=null),a=T.next,T.next=null,e.call(t,T.value,T.worked),c===null&&d.empty()):--u===0&&d.drain()}function g(){a=null,c=null,d.drain=me}function v(){a=null,c=null,d.drain(),d.drain=me}function F(A){x=A}}function me(){}function gm(){this.value=null,this.callback=me,this.next=null,this.release=me,this.context=null,this.errorHandler=null;var t=this;this.worked=function(r,i){var a=t.callback,c=t.errorHandler,u=t.value;t.value=null,t.callback=me,t.errorHandler&&c(r,u),a.call(t.context,r,i),t.release(t)}}function Dm(t,e,r){typeof t=="function"&&(r=e,e=t,t=null);function i(h,m){e.call(this,h).then(function(l){m(null,l)},m)}var a=Xu(t,i,r),c=a.push,u=a.unshift;return a.push=x,a.unshift=d,a.drained=p,a;function x(h){var m=new Promise(function(l,n){c(h,function(s,o){if(s){n(s);return}l(o)})});return m.catch(me),m}function d(h){var m=new Promise(function(l,n){u(h,function(s,o){if(s){n(s);return}l(o)})});return m.catch(me),m}function p(){if(a.idle())return new Promise(function(l){l()});var h=a.drain,m=new Promise(function(l){a.drain=function(){h(),l()}});return m}}Gi.exports=Xu;Gi.exports.promise=Dm});var cr=_(be=>{"use strict";Object.defineProperty(be,"__esModule",{value:!0});be.joinPathSegments=be.replacePathSegmentSeparator=be.isAppliedFilter=be.isFatalError=void 0;function ym(t,e){return t.errorFilter===null?!0:!t.errorFilter(e)}be.isFatalError=ym;function vm(t,e){return t===null||t(e)}be.isAppliedFilter=vm;function Sm(t,e){return t.split(/[/\\]/).join(e)}be.replacePathSegmentSeparator=Sm;function Am(t,e,r){return t===""?e:t.endsWith(r)?t+e:t+r+e}be.joinPathSegments=Am});var Vi=_(Wi=>{"use strict";Object.defineProperty(Wi,"__esModule",{value:!0});var Cm=cr(),Ki=class{constructor(e,r){this._root=e,this._settings=r,this._root=Cm.replacePathSegmentSeparator(e,r.pathSegmentSeparator)}};Wi.default=Ki});var Zi=_(Qi=>{"use strict";Object.defineProperty(Qi,"__esModule",{value:!0});var bm=O("events"),Fm=ur(),wm=Uu(),lr=cr(),_m=Vi(),Yi=class extends _m.default{constructor(e,r){super(e,r),this._settings=r,this._scandir=Fm.scandir,this._emitter=new bm.EventEmitter,this._queue=wm(this._worker.bind(this),this._settings.concurrency),this._isFatalError=!1,this._isDestroyed=!1,this._queue.drain=()=>{this._isFatalError||this._emitter.emit("end")}}read(){return this._isFatalError=!1,this._isDestroyed=!1,setImmediate(()=>{this._pushToQueue(this._root,this._settings.basePath)}),this._emitter}get isDestroyed(){return this._isDestroyed}destroy(){if(this._isDestroyed)throw new Error("The reader is already destroyed");this._isDestroyed=!0,this._queue.killAndDrain()}onEntry(e){this._emitter.on("entry",e)}onError(e){this._emitter.once("error",e)}onEnd(e){this._emitter.once("end",e)}_pushToQueue(e,r){let i={directory:e,base:r};this._queue.push(i,a=>{a!==null&&this._handleError(a)})}_worker(e,r){this._scandir(e.directory,this._settings.fsScandirSettings,(i,a)=>{if(i!==null){r(i,void 0);return}for(let c of a)this._handleEntry(c,e.base);r(null,void 0)})}_handleError(e){this._isDestroyed||!lr.isFatalError(this._settings,e)||(this._isFatalError=!0,this._isDestroyed=!0,this._emitter.emit("error",e))}_handleEntry(e,r){if(this._isDestroyed||this._isFatalError)return;let i=e.path;r!==void 0&&(e.path=lr.joinPathSegments(r,e.name,this._settings.pathSegmentSeparator)),lr.isAppliedFilter(this._settings.entryFilter,e)&&this._emitEntry(e),e.dirent.isDirectory()&&lr.isAppliedFilter(this._settings.deepFilter,e)&&this._pushToQueue(i,r===void 0?void 0:e.path)}_emitEntry(e){this._emitter.emit("entry",e)}};Qi.default=Yi});var $u=_(tn=>{"use strict";Object.defineProperty(tn,"__esModule",{value:!0});var km=Zi(),en=class{constructor(e,r){this._root=e,this._settings=r,this._reader=new km.default(this._root,this._settings),this._storage=[]}read(e){this._reader.onError(r=>{Tm(e,r)}),this._reader.onEntry(r=>{this._storage.push(r)}),this._reader.onEnd(()=>{Bm(e,this._storage)}),this._reader.read()}};tn.default=en;function Tm(t,e){t(e)}function Bm(t,e){t(null,e)}});var ju=_(nn=>{"use strict";Object.defineProperty(nn,"__esModule",{value:!0});var Pm=O("stream"),Rm=Zi(),rn=class{constructor(e,r){this._root=e,this._settings=r,this._reader=new Rm.default(this._root,this._settings),this._stream=new Pm.Readable({objectMode:!0,read:()=>{},destroy:()=>{this._reader.isDestroyed||this._reader.destroy()}})}read(){return this._reader.onError(e=>{this._stream.emit("error",e)}),this._reader.onEntry(e=>{this._stream.push(e)}),this._reader.onEnd(()=>{this._stream.push(null)}),this._reader.read(),this._stream}};nn.default=rn});var Ju=_(an=>{"use strict";Object.defineProperty(an,"__esModule",{value:!0});var Im=ur(),hr=cr(),Nm=Vi(),sn=class extends Nm.default{constructor(){super(...arguments),this._scandir=Im.scandirSync,this._storage=[],this._queue=new Set}read(){return this._pushToQueue(this._root,this._settings.basePath),this._handleQueue(),this._storage}_pushToQueue(e,r){this._queue.add({directory:e,base:r})}_handleQueue(){for(let e of this._queue.values())this._handleDirectory(e.directory,e.base)}_handleDirectory(e,r){try{let i=this._scandir(e,this._settings.fsScandirSettings);for(let a of i)this._handleEntry(a,r)}catch(i){this._handleError(i)}}_handleError(e){if(hr.isFatalError(this._settings,e))throw e}_handleEntry(e,r){let i=e.path;r!==void 0&&(e.path=hr.joinPathSegments(r,e.name,this._settings.pathSegmentSeparator)),hr.isAppliedFilter(this._settings.entryFilter,e)&&this._pushToStorage(e),e.dirent.isDirectory()&&hr.isAppliedFilter(this._settings.deepFilter,e)&&this._pushToQueue(i,r===void 0?void 0:e.path)}_pushToStorage(e){this._storage.push(e)}};an.default=sn});var zu=_(un=>{"use strict";Object.defineProperty(un,"__esModule",{value:!0});var Lm=Ju(),on=class{constructor(e,r){this._root=e,this._settings=r,this._reader=new Lm.default(this._root,this._settings)}read(){return this._reader.read()}};un.default=on});var qu=_(ln=>{"use strict";Object.defineProperty(ln,"__esModule",{value:!0});var Om=O("path"),Mm=ur(),cn=class{constructor(e={}){this._options=e,this.basePath=this._getValue(this._options.basePath,void 0),this.concurrency=this._getValue(this._options.concurrency,Number.POSITIVE_INFINITY),this.deepFilter=this._getValue(this._options.deepFilter,null),this.entryFilter=this._getValue(this._options.entryFilter,null),this.errorFilter=this._getValue(this._options.errorFilter,null),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,Om.sep),this.fsScandirSettings=new Mm.Settings({followSymbolicLinks:this._options.followSymbolicLinks,fs:this._options.fs,pathSegmentSeparator:this._options.pathSegmentSeparator,stats:this._options.stats,throwErrorOnBrokenSymbolicLink:this._options.throwErrorOnBrokenSymbolicLink})}_getValue(e,r){return e??r}};ln.default=cn});var pr=_(Fe=>{"use strict";Object.defineProperty(Fe,"__esModule",{value:!0});Fe.Settings=Fe.walkStream=Fe.walkSync=Fe.walk=void 0;var Gu=$u(),Hm=ju(),Xm=zu(),hn=qu();Fe.Settings=hn.default;function Um(t,e,r){if(typeof e=="function"){new Gu.default(t,fr()).read(e);return}new Gu.default(t,fr(e)).read(r)}Fe.walk=Um;function $m(t,e){let r=fr(e);return new Xm.default(t,r).read()}Fe.walkSync=$m;function jm(t,e){let r=fr(e);return new Hm.default(t,r).read()}Fe.walkStream=jm;function fr(t={}){return t instanceof hn.default?t:new hn.default(t)}});var dr=_(pn=>{"use strict";Object.defineProperty(pn,"__esModule",{value:!0});var Jm=O("path"),zm=Ge(),Ku=Te(),fn=class{constructor(e){this._settings=e,this._fsStatSettings=new zm.Settings({followSymbolicLink:this._settings.followSymbolicLinks,fs:this._settings.fs,throwErrorOnBrokenSymbolicLink:this._settings.followSymbolicLinks})}_getFullEntryPath(e){return Jm.resolve(this._settings.cwd,e)}_makeEntry(e,r){let i={name:r,path:r,dirent:Ku.fs.createDirentFromStats(r,e)};return this._settings.stats&&(i.stats=e),i}_isFatalError(e){return!Ku.errno.isEnoentCodeError(e)&&!this._settings.suppressErrors}};pn.default=fn});var xn=_(mn=>{"use strict";Object.defineProperty(mn,"__esModule",{value:!0});var qm=O("stream"),Gm=Ge(),Km=pr(),Wm=dr(),dn=class extends Wm.default{constructor(){super(...arguments),this._walkStream=Km.walkStream,this._stat=Gm.stat}dynamic(e,r){return this._walkStream(e,r)}static(e,r){let i=e.map(this._getFullEntryPath,this),a=new qm.PassThrough({objectMode:!0});a._write=(c,u,x)=>this._getEntry(i[c],e[c],r).then(d=>{d!==null&&r.entryFilter(d)&&a.push(d),c===i.length-1&&a.end(),x()}).catch(x);for(let c=0;cthis._makeEntry(a,r)).catch(a=>{if(i.errorFilter(a))return null;throw a})}_getStat(e){return new Promise((r,i)=>{this._stat(e,this._fsStatSettings,(a,c)=>a===null?r(c):i(a))})}};mn.default=dn});var Wu=_(gn=>{"use strict";Object.defineProperty(gn,"__esModule",{value:!0});var Vm=pr(),Ym=dr(),Qm=xn(),En=class extends Ym.default{constructor(){super(...arguments),this._walkAsync=Vm.walk,this._readerStream=new Qm.default(this._settings)}dynamic(e,r){return new Promise((i,a)=>{this._walkAsync(e,r,(c,u)=>{c===null?i(u):a(c)})})}async static(e,r){let i=[],a=this._readerStream.static(e,r);return new Promise((c,u)=>{a.once("error",u),a.on("data",x=>i.push(x)),a.once("end",()=>c(i))})}};gn.default=En});var Vu=_(yn=>{"use strict";Object.defineProperty(yn,"__esModule",{value:!0});var bt=Te(),Dn=class{constructor(e,r,i){this._patterns=e,this._settings=r,this._micromatchOptions=i,this._storage=[],this._fillStorage()}_fillStorage(){for(let e of this._patterns){let r=this._getPatternSegments(e),i=this._splitSegmentsIntoSections(r);this._storage.push({complete:i.length<=1,pattern:e,segments:r,sections:i})}}_getPatternSegments(e){return bt.pattern.getPatternParts(e,this._micromatchOptions).map(i=>bt.pattern.isDynamicPattern(i,this._settings)?{dynamic:!0,pattern:i,patternRe:bt.pattern.makeRe(i,this._micromatchOptions)}:{dynamic:!1,pattern:i})}_splitSegmentsIntoSections(e){return bt.array.splitWhen(e,r=>r.dynamic&&bt.pattern.hasGlobStar(r.pattern))}};yn.default=Dn});var Yu=_(Sn=>{"use strict";Object.defineProperty(Sn,"__esModule",{value:!0});var Zm=Vu(),vn=class extends Zm.default{match(e){let r=e.split("/"),i=r.length,a=this._storage.filter(c=>!c.complete||c.segments.length>i);for(let c of a){let u=c.sections[0];if(!c.complete&&i>u.length||r.every((d,p)=>{let h=c.segments[p];return!!(h.dynamic&&h.patternRe.test(d)||!h.dynamic&&h.pattern===d)}))return!0}return!1}};Sn.default=vn});var Qu=_(Cn=>{"use strict";Object.defineProperty(Cn,"__esModule",{value:!0});var mr=Te(),e0=Yu(),An=class{constructor(e,r){this._settings=e,this._micromatchOptions=r}getFilter(e,r,i){let a=this._getMatcher(r),c=this._getNegativePatternsRe(i);return u=>this._filter(e,u,a,c)}_getMatcher(e){return new e0.default(e,this._settings,this._micromatchOptions)}_getNegativePatternsRe(e){let r=e.filter(mr.pattern.isAffectDepthOfReadingPattern);return mr.pattern.convertPatternsToRe(r,this._micromatchOptions)}_filter(e,r,i,a){if(this._isSkippedByDeep(e,r.path)||this._isSkippedSymbolicLink(r))return!1;let c=mr.path.removeLeadingDotSegment(r.path);return this._isSkippedByPositivePatterns(c,i)?!1:this._isSkippedByNegativePatterns(c,a)}_isSkippedByDeep(e,r){return this._settings.deep===1/0?!1:this._getEntryLevel(e,r)>=this._settings.deep}_getEntryLevel(e,r){let i=r.split("/").length;if(e==="")return i;let a=e.split("/").length;return i-a}_isSkippedSymbolicLink(e){return!this._settings.followSymbolicLinks&&e.dirent.isSymbolicLink()}_isSkippedByPositivePatterns(e,r){return!this._settings.baseNameMatch&&!r.match(e)}_isSkippedByNegativePatterns(e,r){return!mr.pattern.matchAny(e,r)}};Cn.default=An});var Zu=_(Fn=>{"use strict";Object.defineProperty(Fn,"__esModule",{value:!0});var Ke=Te(),bn=class{constructor(e,r){this._settings=e,this._micromatchOptions=r,this.index=new Map}getFilter(e,r){let i=Ke.pattern.convertPatternsToRe(e,this._micromatchOptions),a=Ke.pattern.convertPatternsToRe(r,Object.assign(Object.assign({},this._micromatchOptions),{dot:!0}));return c=>this._filter(c,i,a)}_filter(e,r,i){let a=Ke.path.removeLeadingDotSegment(e.path);if(this._settings.unique&&this._isDuplicateEntry(a)||this._onlyFileFilter(e)||this._onlyDirectoryFilter(e)||this._isSkippedByAbsoluteNegativePatterns(a,i))return!1;let c=e.dirent.isDirectory(),u=this._isMatchToPatterns(a,r,c)&&!this._isMatchToPatterns(a,i,c);return this._settings.unique&&u&&this._createIndexRecord(a),u}_isDuplicateEntry(e){return this.index.has(e)}_createIndexRecord(e){this.index.set(e,void 0)}_onlyFileFilter(e){return this._settings.onlyFiles&&!e.dirent.isFile()}_onlyDirectoryFilter(e){return this._settings.onlyDirectories&&!e.dirent.isDirectory()}_isSkippedByAbsoluteNegativePatterns(e,r){if(!this._settings.absolute)return!1;let i=Ke.path.makeAbsolute(this._settings.cwd,e);return Ke.pattern.matchAny(i,r)}_isMatchToPatterns(e,r,i){let a=Ke.pattern.matchAny(e,r);return!a&&i?Ke.pattern.matchAny(e+"/",r):a}};Fn.default=bn});var ec=_(_n=>{"use strict";Object.defineProperty(_n,"__esModule",{value:!0});var t0=Te(),wn=class{constructor(e){this._settings=e}getFilter(){return e=>this._isNonFatalError(e)}_isNonFatalError(e){return t0.errno.isEnoentCodeError(e)||this._settings.suppressErrors}};_n.default=wn});var rc=_(Tn=>{"use strict";Object.defineProperty(Tn,"__esModule",{value:!0});var tc=Te(),kn=class{constructor(e){this._settings=e}getTransformer(){return e=>this._transform(e)}_transform(e){let r=e.path;return this._settings.absolute&&(r=tc.path.makeAbsolute(this._settings.cwd,r),r=tc.path.unixify(r)),this._settings.markDirectories&&e.dirent.isDirectory()&&(r+="/"),this._settings.objectMode?Object.assign(Object.assign({},e),{path:r}):r}};Tn.default=kn});var xr=_(Pn=>{"use strict";Object.defineProperty(Pn,"__esModule",{value:!0});var r0=O("path"),i0=Qu(),n0=Zu(),s0=ec(),a0=rc(),Bn=class{constructor(e){this._settings=e,this.errorFilter=new s0.default(this._settings),this.entryFilter=new n0.default(this._settings,this._getMicromatchOptions()),this.deepFilter=new i0.default(this._settings,this._getMicromatchOptions()),this.entryTransformer=new a0.default(this._settings)}_getRootDirectory(e){return r0.resolve(this._settings.cwd,e.base)}_getReaderOptions(e){let r=e.base==="."?"":e.base;return{basePath:r,pathSegmentSeparator:"/",concurrency:this._settings.concurrency,deepFilter:this.deepFilter.getFilter(r,e.positive,e.negative),entryFilter:this.entryFilter.getFilter(e.positive,e.negative),errorFilter:this.errorFilter.getFilter(),followSymbolicLinks:this._settings.followSymbolicLinks,fs:this._settings.fs,stats:this._settings.stats,throwErrorOnBrokenSymbolicLink:this._settings.throwErrorOnBrokenSymbolicLink,transform:this.entryTransformer.getTransformer()}}_getMicromatchOptions(){return{dot:this._settings.dot,matchBase:this._settings.baseNameMatch,nobrace:!this._settings.braceExpansion,nocase:!this._settings.caseSensitiveMatch,noext:!this._settings.extglob,noglobstar:!this._settings.globstar,posix:!0,strictSlashes:!1}}};Pn.default=Bn});var ic=_(In=>{"use strict";Object.defineProperty(In,"__esModule",{value:!0});var o0=Wu(),u0=xr(),Rn=class extends u0.default{constructor(){super(...arguments),this._reader=new o0.default(this._settings)}async read(e){let r=this._getRootDirectory(e),i=this._getReaderOptions(e);return(await this.api(r,e,i)).map(c=>i.transform(c))}api(e,r,i){return r.dynamic?this._reader.dynamic(e,i):this._reader.static(r.patterns,i)}};In.default=Rn});var nc=_(Ln=>{"use strict";Object.defineProperty(Ln,"__esModule",{value:!0});var c0=O("stream"),l0=xn(),h0=xr(),Nn=class extends h0.default{constructor(){super(...arguments),this._reader=new l0.default(this._settings)}read(e){let r=this._getRootDirectory(e),i=this._getReaderOptions(e),a=this.api(r,e,i),c=new c0.Readable({objectMode:!0,read:()=>{}});return a.once("error",u=>c.emit("error",u)).on("data",u=>c.emit("data",i.transform(u))).once("end",()=>c.emit("end")),c.once("close",()=>a.destroy()),c}api(e,r,i){return r.dynamic?this._reader.dynamic(e,i):this._reader.static(r.patterns,i)}};Ln.default=Nn});var sc=_(Mn=>{"use strict";Object.defineProperty(Mn,"__esModule",{value:!0});var f0=Ge(),p0=pr(),d0=dr(),On=class extends d0.default{constructor(){super(...arguments),this._walkSync=p0.walkSync,this._statSync=f0.statSync}dynamic(e,r){return this._walkSync(e,r)}static(e,r){let i=[];for(let a of e){let c=this._getFullEntryPath(a),u=this._getEntry(c,a,r);u===null||!r.entryFilter(u)||i.push(u)}return i}_getEntry(e,r,i){try{let a=this._getStat(e);return this._makeEntry(a,r)}catch(a){if(i.errorFilter(a))return null;throw a}}_getStat(e){return this._statSync(e,this._fsStatSettings)}};Mn.default=On});var ac=_(Xn=>{"use strict";Object.defineProperty(Xn,"__esModule",{value:!0});var m0=sc(),x0=xr(),Hn=class extends x0.default{constructor(){super(...arguments),this._reader=new m0.default(this._settings)}read(e){let r=this._getRootDirectory(e),i=this._getReaderOptions(e);return this.api(r,e,i).map(i.transform)}api(e,r,i){return r.dynamic?this._reader.dynamic(e,i):this._reader.static(r.patterns,i)}};Xn.default=Hn});var oc=_(ct=>{"use strict";Object.defineProperty(ct,"__esModule",{value:!0});ct.DEFAULT_FILE_SYSTEM_ADAPTER=void 0;var ut=O("fs"),E0=O("os"),g0=Math.max(E0.cpus().length,1);ct.DEFAULT_FILE_SYSTEM_ADAPTER={lstat:ut.lstat,lstatSync:ut.lstatSync,stat:ut.stat,statSync:ut.statSync,readdir:ut.readdir,readdirSync:ut.readdirSync};var Un=class{constructor(e={}){this._options=e,this.absolute=this._getValue(this._options.absolute,!1),this.baseNameMatch=this._getValue(this._options.baseNameMatch,!1),this.braceExpansion=this._getValue(this._options.braceExpansion,!0),this.caseSensitiveMatch=this._getValue(this._options.caseSensitiveMatch,!0),this.concurrency=this._getValue(this._options.concurrency,g0),this.cwd=this._getValue(this._options.cwd,process.cwd()),this.deep=this._getValue(this._options.deep,1/0),this.dot=this._getValue(this._options.dot,!1),this.extglob=this._getValue(this._options.extglob,!0),this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!0),this.fs=this._getFileSystemMethods(this._options.fs),this.globstar=this._getValue(this._options.globstar,!0),this.ignore=this._getValue(this._options.ignore,[]),this.markDirectories=this._getValue(this._options.markDirectories,!1),this.objectMode=this._getValue(this._options.objectMode,!1),this.onlyDirectories=this._getValue(this._options.onlyDirectories,!1),this.onlyFiles=this._getValue(this._options.onlyFiles,!0),this.stats=this._getValue(this._options.stats,!1),this.suppressErrors=this._getValue(this._options.suppressErrors,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!1),this.unique=this._getValue(this._options.unique,!0),this.onlyDirectories&&(this.onlyFiles=!1),this.stats&&(this.objectMode=!0),this.ignore=[].concat(this.ignore)}_getValue(e,r){return e===void 0?r:e}_getFileSystemMethods(e={}){return Object.assign(Object.assign({},ct.DEFAULT_FILE_SYSTEM_ADAPTER),e)}};ct.default=Un});var zn=_((_E,cc)=>{"use strict";var uc=cu(),D0=ic(),y0=nc(),v0=ac(),$n=oc(),xe=Te();async function jn(t,e){De(t);let r=Jn(t,D0.default,e),i=await Promise.all(r);return xe.array.flatten(i)}(function(t){t.glob=t,t.globSync=e,t.globStream=r,t.async=t;function e(p,h){De(p);let m=Jn(p,v0.default,h);return xe.array.flatten(m)}t.sync=e;function r(p,h){De(p);let m=Jn(p,y0.default,h);return xe.stream.merge(m)}t.stream=r;function i(p,h){De(p);let m=[].concat(p),l=new $n.default(h);return uc.generate(m,l)}t.generateTasks=i;function a(p,h){De(p);let m=new $n.default(h);return xe.pattern.isDynamicPattern(p,m)}t.isDynamicPattern=a;function c(p){return De(p),xe.path.escape(p)}t.escapePath=c;function u(p){return De(p),xe.path.convertPathToPattern(p)}t.convertPathToPattern=u;let x;(function(p){function h(l){return De(l),xe.path.escapePosixPath(l)}p.escapePath=h;function m(l){return De(l),xe.path.convertPosixPathToPattern(l)}p.convertPathToPattern=m})(x=t.posix||(t.posix={}));let d;(function(p){function h(l){return De(l),xe.path.escapeWindowsPath(l)}p.escapePath=h;function m(l){return De(l),xe.path.convertWindowsPathToPattern(l)}p.convertPathToPattern=m})(d=t.win32||(t.win32={}))})(jn||(jn={}));function Jn(t,e,r){let i=[].concat(t),a=new $n.default(r),c=uc.generate(i,a),u=new e(a);return c.map(u.read,u)}function De(t){if(![].concat(t).every(i=>xe.string.isString(i)&&!xe.string.isEmpty(i)))throw new TypeError("Patterns must be a string (non empty) or an array of strings")}cc.exports=jn});var vc=_((LE,yc)=>{function pc(t){return Array.isArray(t)?t:[t]}var Vn="",dc=" ",Kn="\\",S0=/^\s+$/,A0=/(?:[^\\]|^)\\$/,C0=/^\\!/,b0=/^\\#/,F0=/\r?\n/g,w0=/^\.*\/|^\.+$/,Wn="/",Ec="node-ignore";typeof Symbol<"u"&&(Ec=Symbol.for("node-ignore"));var mc=Ec,_0=(t,e,r)=>Object.defineProperty(t,e,{value:r}),k0=/([0-z])-([0-z])/g,gc=()=>!1,T0=t=>t.replace(k0,(e,r,i)=>r.charCodeAt(0)<=i.charCodeAt(0)?e:Vn),B0=t=>{let{length:e}=t;return t.slice(0,e-e%2)},P0=[[/^\uFEFF/,()=>Vn],[/((?:\\\\)*?)(\\?\s+)$/,(t,e,r)=>e+(r.indexOf("\\")===0?dc:Vn)],[/(\\+?)\s/g,(t,e)=>{let{length:r}=e;return e.slice(0,r-r%2)+dc}],[/[\\$.|*+(){^]/g,t=>`\\${t}`],[/(?!\\)\?/g,()=>"[^/]"],[/^\//,()=>"^"],[/\//g,()=>"\\/"],[/^\^*\\\*\\\*\\\//,()=>"^(?:.*\\/)?"],[/^(?=[^^])/,function(){return/\/(?!$)/.test(this)?"^":"(?:^|\\/)"}],[/\\\/\\\*\\\*(?=\\\/|$)/g,(t,e,r)=>e+6{let i=r.replace(/\\\*/g,"[^\\/]*");return e+i}],[/\\\\\\(?=[$.|*+(){^])/g,()=>Kn],[/\\\\/g,()=>Kn],[/(\\)?\[([^\]/]*?)(\\*)($|\])/g,(t,e,r,i,a)=>e===Kn?`\\[${r}${B0(i)}${a}`:a==="]"&&i.length%2===0?`[${T0(r)}${i}]`:"[]"],[/(?:[^*])$/,t=>/\/$/.test(t)?`${t}$`:`${t}(?=$|\\/$)`],[/(\^|\\\/)?\\\*$/,(t,e)=>`${e?`${e}[^/]+`:"[^/]*"}(?=$|\\/$)`]],xc=Object.create(null),R0=(t,e)=>{let r=xc[t];return r||(r=P0.reduce((i,[a,c])=>i.replace(a,c.bind(t)),t),xc[t]=r),e?new RegExp(r,"i"):new RegExp(r)},Zn=t=>typeof t=="string",I0=t=>t&&Zn(t)&&!S0.test(t)&&!A0.test(t)&&t.indexOf("#")!==0,N0=t=>t.split(F0),Yn=class{constructor(e,r,i,a){this.origin=e,this.pattern=r,this.negative=i,this.regex=a}},L0=(t,e)=>{let r=t,i=!1;t.indexOf("!")===0&&(i=!0,t=t.substr(1)),t=t.replace(C0,"!").replace(b0,"#");let a=R0(t,e);return new Yn(r,t,i,a)},O0=(t,e)=>{throw new e(t)},Be=(t,e,r)=>Zn(t)?t?Be.isNotRelative(t)?r(`path should be a \`path.relative()\`d string, but got "${e}"`,RangeError):!0:r("path must not be empty",TypeError):r(`path must be a string, but got \`${e}\``,TypeError),Dc=t=>w0.test(t);Be.isNotRelative=Dc;Be.convert=t=>t;var Qn=class{constructor({ignorecase:e=!0,ignoreCase:r=e,allowRelativePaths:i=!1}={}){_0(this,mc,!0),this._rules=[],this._ignoreCase=r,this._allowRelativePaths=i,this._initCache()}_initCache(){this._ignoreCache=Object.create(null),this._testCache=Object.create(null)}_addPattern(e){if(e&&e[mc]){this._rules=this._rules.concat(e._rules),this._added=!0;return}if(I0(e)){let r=L0(e,this._ignoreCase);this._added=!0,this._rules.push(r)}}add(e){return this._added=!1,pc(Zn(e)?N0(e):e).forEach(this._addPattern,this),this._added&&this._initCache(),this}addPattern(e){return this.add(e)}_testOne(e,r){let i=!1,a=!1;return this._rules.forEach(c=>{let{negative:u}=c;if(a===u&&i!==a||u&&!i&&!a&&!r)return;c.regex.test(e)&&(i=!u,a=u)}),{ignored:i,unignored:a}}_test(e,r,i,a){let c=e&&Be.convert(e);return Be(c,e,this._allowRelativePaths?gc:O0),this._t(c,r,i,a)}_t(e,r,i,a){if(e in r)return r[e];if(a||(a=e.split(Wn)),a.pop(),!a.length)return r[e]=this._testOne(e,i);let c=this._t(a.join(Wn)+Wn,r,i,a);return r[e]=c.ignored?c:this._testOne(e,i)}ignores(e){return this._test(e,this._ignoreCache,!1).ignored}createFilter(){return e=>!this.ignores(e)}filter(e){return pc(e).filter(this.createFilter())}test(e){return this._test(e,this._testCache,!0)}},gr=t=>new Qn(t),M0=t=>Be(t&&Be.convert(t),t,gc);gr.isPathValid=M0;gr.default=gr;yc.exports=gr;if(typeof process<"u"&&(process.env&&process.env.IGNORE_TEST_WIN32||process.platform==="win32")){let t=r=>/^\\\\\?\\/.test(r)||/["<>|\u0000-\u001F]+/u.test(r)?r:r.replace(/\\/g,"/");Be.convert=t;let e=/^[a-z]:\//i;Be.isNotRelative=r=>e.test(r)||Dc(r)}});var V0={};Fl(V0,{default:()=>W0});var Ue=O("path");var $e=O("path");function os(t){t=t.replace(/\\/g,"/");let e=t,r=[t];return t.endsWith(".json")?(r=[t],e=$e.posix.join(t,"..","package.json"),t=$e.posix.join(t,"..")):(r=[$e.posix.join(t,"tsconfig.json"),$e.posix.join(t,"tsconfig.*.json")],e=$e.posix.join(t,"package.json")),{directory:$e.posix.resolve(t),packageJsonPath:e,tsConfigPatterns:r}}var _t=O("fs/promises"),Ee=ne(ls(),1),ft=ne(ti(),1);var ts=ne(O("process"),1),Ic=ne(O("fs"),1),Ve=ne(O("path"),1);var Ut=O("events"),Ea=O("stream"),si=O("stream/promises");function ai(t){if(!Array.isArray(t))throw new TypeError(`Expected an array, got \`${typeof t}\`.`);for(let a of t)ii(a);let e=t.some(({readableObjectMode:a})=>a),r=uf(t,e),i=new ri({objectMode:e,writableHighWaterMark:r,readableHighWaterMark:r});for(let a of t)i.add(a);return t.length===0&&ya(i),i}var uf=(t,e)=>{if(t.length===0)return 16384;let r=t.filter(({readableObjectMode:i})=>i===e).map(({readableHighWaterMark:i})=>i);return Math.max(...r)},ri=class extends Ea.PassThrough{#e=new Set([]);#r=new Set([]);#i=new Set([]);#t;add(e){ii(e),!this.#e.has(e)&&(this.#e.add(e),this.#t??=cf(this,this.#e),ff({passThroughStream:this,stream:e,streams:this.#e,ended:this.#r,aborted:this.#i,onFinished:this.#t}),e.pipe(this,{end:!1}))}remove(e){return ii(e),this.#e.has(e)?(e.unpipe(this),!0):!1}},cf=async(t,e)=>{Xt(t,ma);let r=new AbortController;try{await Promise.race([lf(t,r),hf(t,e,r)])}finally{r.abort(),Xt(t,-ma)}},lf=async(t,{signal:e})=>{await(0,si.finished)(t,{signal:e,cleanup:!0})},hf=async(t,e,{signal:r})=>{for await(let[i]of(0,Ut.on)(t,"unpipe",{signal:r}))e.has(i)&&i.emit(Da)},ii=t=>{if(typeof t?.pipe!="function")throw new TypeError(`Expected a readable stream, got: \`${typeof t}\`.`)},ff=async({passThroughStream:t,stream:e,streams:r,ended:i,aborted:a,onFinished:c})=>{Xt(t,xa);let u=new AbortController;try{await Promise.race([pf(c,e),df({passThroughStream:t,stream:e,streams:r,ended:i,aborted:a,controller:u}),mf({stream:e,streams:r,ended:i,aborted:a,controller:u})])}finally{u.abort(),Xt(t,-xa)}r.size===i.size+a.size&&(i.size===0&&a.size>0?ni(t):ya(t))},ga=t=>t?.code==="ERR_STREAM_PREMATURE_CLOSE",pf=async(t,e)=>{try{await t,ni(e)}catch(r){ga(r)?ni(e):va(e,r)}},df=async({passThroughStream:t,stream:e,streams:r,ended:i,aborted:a,controller:{signal:c}})=>{try{await(0,si.finished)(e,{signal:c,cleanup:!0,readable:!0,writable:!1}),r.has(e)&&i.add(e)}catch(u){if(c.aborted||!r.has(e))return;ga(u)?a.add(e):va(t,u)}},mf=async({stream:t,streams:e,ended:r,aborted:i,controller:{signal:a}})=>{await(0,Ut.once)(t,Da,{signal:a}),e.delete(t),r.delete(t),i.delete(t)},Da=Symbol("unpipe"),ya=t=>{t.writable&&t.end()},ni=t=>{(t.readable||t.writable)&&t.destroy()},va=(t,e)=>{t.destroyed||(t.once("error",xf),t.destroy(e))},xf=()=>{},Xt=(t,e)=>{let r=t.getMaxListeners();r!==0&&r!==Number.POSITIVE_INFINITY&&t.setMaxListeners(r+e)},ma=2,xa=1;var ht=ne(zn(),1);var Er=ne(O("fs"),1);async function qn(t,e,r){if(typeof r!="string")throw new TypeError(`Expected a string, got ${typeof r}`);try{return(await Er.promises[t](r))[e]()}catch(i){if(i.code==="ENOENT")return!1;throw i}}function Gn(t,e,r){if(typeof r!="string")throw new TypeError(`Expected a string, got ${typeof r}`);try{return Er.default[t](r)[e]()}catch(i){if(i.code==="ENOENT")return!1;throw i}}var kE=qn.bind(null,"stat","isFile"),lc=qn.bind(null,"stat","isDirectory"),TE=qn.bind(null,"lstat","isSymbolicLink"),BE=Gn.bind(null,"statSync","isFile"),hc=Gn.bind(null,"statSync","isDirectory"),PE=Gn.bind(null,"lstatSync","isSymbolicLink");var fc=O("url");function Ft(t){return t instanceof URL?(0,fc.fileURLToPath)(t):t}var Sc=ne(O("process"),1),Ac=ne(O("fs"),1),Cc=ne(O("fs/promises"),1),We=ne(O("path"),1),es=ne(zn(),1),bc=ne(vc(),1);function lt(t){return t.startsWith("\\\\?\\")?t:t.replace(/\\/g,"/")}var wt=t=>t[0]==="!";var H0=["**/node_modules","**/flow-typed","**/coverage","**/.git"],Fc={absolute:!0,dot:!0},wc="**/.gitignore",X0=(t,e)=>wt(t)?"!"+We.default.posix.join(e,t.slice(1)):We.default.posix.join(e,t),U0=(t,e)=>{let r=lt(We.default.relative(e,We.default.dirname(t.filePath)));return t.content.split(/\r?\n/).filter(i=>i&&!i.startsWith("#")).map(i=>X0(i,r))},$0=(t,e)=>{if(e=lt(e),We.default.isAbsolute(t)){if(lt(t).startsWith(e))return We.default.relative(e,t);throw new Error(`Path ${t} is not in cwd ${e}`)}return t},_c=(t,e)=>{let r=t.flatMap(a=>U0(a,e)),i=(0,bc.default)().add(r);return a=>(a=Ft(a),a=$0(a,e),a?i.ignores(lt(a)):!1)},kc=(t={})=>({cwd:Ft(t.cwd)??Sc.default.cwd(),suppressErrors:!!t.suppressErrors,deep:typeof t.deep=="number"?t.deep:Number.POSITIVE_INFINITY,ignore:[...t.ignore??[],...H0]}),Tc=async(t,e)=>{let{cwd:r,suppressErrors:i,deep:a,ignore:c}=kc(e),u=await(0,es.default)(t,{cwd:r,suppressErrors:i,deep:a,ignore:c,...Fc}),x=await Promise.all(u.map(async d=>({filePath:d,content:await Cc.default.readFile(d,"utf8")})));return _c(x,r)},Bc=(t,e)=>{let{cwd:r,suppressErrors:i,deep:a,ignore:c}=kc(e),x=es.default.sync(t,{cwd:r,suppressErrors:i,deep:a,ignore:c,...Fc}).map(d=>({filePath:d,content:Ac.default.readFileSync(d,"utf8")}));return _c(x,r)};var j0=t=>{if(t.some(e=>typeof e!="string"))throw new TypeError("Patterns must be a string or an array of strings")},Nc=(t,e)=>{let r=wt(t)?t.slice(1):t;return Ve.default.isAbsolute(r)?r:Ve.default.join(e,r)},Lc=({directoryPath:t,files:e,extensions:r})=>{let i=r?.length>0?`.${r.length>1?`{${r.join(",")}}`:r[0]}`:"";return e?e.map(a=>Ve.default.posix.join(t,`**/${Ve.default.extname(a)?a:`${a}${i}`}`)):[Ve.default.posix.join(t,`**${i?`/*${i}`:""}`)]},Pc=async(t,{cwd:e=ts.default.cwd(),files:r,extensions:i}={})=>(await Promise.all(t.map(async c=>await lc(Nc(c,e))?Lc({directoryPath:c,files:r,extensions:i}):c))).flat(),Rc=(t,{cwd:e=ts.default.cwd(),files:r,extensions:i}={})=>t.flatMap(a=>hc(Nc(a,e))?Lc({directoryPath:a,files:r,extensions:i}):a),rs=t=>(t=[...new Set([t].flat())],j0(t),t),J0=t=>{if(!t)return;let e;try{e=Ic.default.statSync(t)}catch{return}if(!e.isDirectory())throw new Error("The `cwd` option must be a path to a directory")},Oc=(t={})=>(t={...t,ignore:t.ignore??[],expandDirectories:t.expandDirectories??!0,cwd:Ft(t.cwd)},J0(t.cwd),t),Mc=t=>async(e,r)=>t(rs(e),Oc(r)),Dr=t=>(e,r)=>t(rs(e),Oc(r)),Hc=t=>{let{ignoreFiles:e,gitignore:r}=t,i=e?rs(e):[];return r&&i.push(wc),i},z0=async t=>{let e=Hc(t);return Uc(e.length>0&&await Tc(e,t))},Xc=t=>{let e=Hc(t);return Uc(e.length>0&&Bc(e,t))},Uc=t=>{let e=new Set;return r=>{let i=Ve.default.normalize(r.path??r);return e.has(i)||t&&t(i)?!1:(e.add(i),!0)}},$c=(t,e)=>t.flat().filter(r=>e(r)),jc=(t,e)=>{let r=[];for(;t.length>0;){let i=t.findIndex(c=>wt(c));if(i===-1){r.push({patterns:t,options:e});break}let a=t[i].slice(1);for(let c of r)c.options.ignore.push(a);i!==0&&r.push({patterns:t.slice(0,i),options:{...e,ignore:[...e.ignore,a]}}),t=t.slice(i+1)}return r},Jc=(t,e)=>({...e?{cwd:e}:{},...Array.isArray(t)?{files:t}:t}),zc=async(t,e)=>{let r=jc(t,e),{cwd:i,expandDirectories:a}=e;if(!a)return r;let c=Jc(a,i);return Promise.all(r.map(async u=>{let{patterns:x,options:d}=u;return[x,d.ignore]=await Promise.all([Pc(x,c),Pc(d.ignore,{cwd:i})]),{patterns:x,options:d}}))},is=(t,e)=>{let r=jc(t,e),{cwd:i,expandDirectories:a}=e;if(!a)return r;let c=Jc(a,i);return r.map(u=>{let{patterns:x,options:d}=u;return x=Rc(x,c),d.ignore=Rc(d.ignore,{cwd:i}),{patterns:x,options:d}})},qc=Mc(async(t,e)=>{let[r,i]=await Promise.all([zc(t,e),z0(e)]),a=await Promise.all(r.map(c=>(0,ht.default)(c.patterns,c.options)));return $c(a,i)}),KE=Dr((t,e)=>{let r=is(t,e),i=Xc(e),a=r.map(c=>ht.default.sync(c.patterns,c.options));return $c(a,i)}),WE=Dr((t,e)=>{let r=is(t,e),i=Xc(e),a=r.map(u=>ht.default.stream(u.patterns,u.options));return ai(a).filter(u=>i(u))}),VE=Dr((t,e)=>t.some(r=>ht.default.isDynamicPattern(r,e))),YE=Mc(zc),QE=Dr(is),{convertPathToPattern:ZE}=ht.default;var ss=ne(O("os"),1);var Gc=O("fs/promises"),Sr=ne(ti(),1),ns=O("path");function yr(t){return t.endsWith("tsconfig.json")}var vr=class{cache;constructor(){this.cache=new Map}async resolve(e){if(this.cache.has(e))return this.cache.get(e);let r=await(0,Gc.readFile)(e,"utf-8").then(i=>Sr.default.parse(i));for(let i of r.references||[])(0,Sr.assign)(i,{path:ns.posix.join(e,"..",i.path)});return this.cache.set(e,r),r}async isReferencedInRootConfig(e){return yr(e)?!1:(await this.resolve(ns.posix.join(e,"..","tsconfig.json"))).references?.some(i=>i.path===e)||!1}};var Wc=O("fs");function Kc(t){return t.replace(/\\/g,"/")}async function Vc(t={},...e){let r=new vr,i=new Map,a=new Map;for(let u of e){let{directory:x,packageJsonPath:d,tsConfigPatterns:p}=os(u);try{let h=await(0,_t.readFile)(d,"utf-8").then(n=>JSON.parse(n));if(!h.name){console.warn(`No name found in package.json at ${d}, skipping`);continue}let m=await qc(p);if(m.length===0)continue;let l={directory:x,tsConfigPaths:m,packageInfo:{name:h.name,dependencies:{...h.dependencies||{},...h.devDependencies||{}}}};a.set(h.name,l),i.set(x,l);for(let n of m)i.set(n,l)}catch{console.warn(`Error reading package.json at ${d}, skipping`)}}let c=!1;for(let[,{directory:u,tsConfigPaths:x,packageInfo:d}]of a)for(let p of x)try{let h=await r.resolve(p),m=new Set,l=new Set,n=[],s=new Set;for(let E of h.references||[]){let g=E.path.endsWith(".json")?E.path:Ue.posix.join(E.path,"tsconfig.json"),v=i.get(g);if(v){if(await r.isReferencedInRootConfig(g)&&!yr(p)){n.push({name:Ue.posix.relative(u,E.path),referencedInRoot:!0});continue}let A=await r.resolve(g),T=!A?.compilerOptions?.composite,k=A?.compilerOptions?.noEmit;if(!(v.packageInfo.name in d.dependencies||v.directory===u)||T||k){n.push({name:v.packageInfo.name,notComposite:T,noEmit:k});continue}}else if((0,Wc.existsSync)(g))m.add(Ue.posix.relative(u,E.path));else{n.push({name:Ue.posix.relative(u,E.path),notFound:!0});continue}s.add(Ue.posix.relative(u,E.path))}for(let E of Object.keys(d.dependencies)){let g=a.get(E);if(g)for(let v of g.tsConfigPaths){if(await r.isReferencedInRootConfig(v))continue;let A=await r.resolve(v);if(!A.compilerOptions?.composite||A.compilerOptions?.noEmit)continue;let T=Ue.posix.relative(u,v.replace("tsconfig.json",""));s.has(T)||(s.add(T),l.add(E))}}if((l.size||m.size||n.length)&&console.log(q0(d.name)+Ee.default.dim(" \xB7"),Ee.default.dim(Ue.posix.relative(Kc(process.cwd()),p))),l.size)for(let E of l)console.log(Ee.default.greenBright(" + ")+Ee.default.dim(`${E}`));if(n.length)for(let E of n){let g="";t.verbose&&(E.noEmit&&(g+=" (noEmit: true)"),E.notComposite&&(g+=" (composite: false)"),E.notFound&&(g+=" (not found)"),E.referencedInRoot&&(g+=" (referenced in root tsconfig.json)")),console.log(Ee.default.redBright(" - ")+Ee.default.dim(`${E.name}${g}`))}if(m.size)for(let E of m)console.log(Ee.default.yellowBright(" ? ")+Ee.default.dim(`${E}`));if(t.immutable){(l.size||n.length)&&(c=!0);continue}let o=await(0,_t.readFile)(p,"utf-8").then(E=>ft.default.parse(E)),f=o.references?.filter(E=>s.has(E.path))||[];for(let E of s)f.some(g=>g.path===E)||f.push({path:E});(0,ft.assign)(o,{references:f.sort((E,g)=>E.path.localeCompare(g.path))}),o.references?.length===0&&delete o.references,await(0,_t.writeFile)(p,G0((0,ft.stringify)(o,null,2))+ss.default.EOL)}catch(h){console.warn(`Error reading tsconfig.json at ${p}, skipping`,h);continue}if(t.verbose){let u=a.size;console.log(Ee.default.dim(Ee.default.greenBright(`Linked ${u} package${u===1?"":"s"}`)))}c&&(console.error("Immutable mode is enabled, please fix the issues manually"),process.exit(1))}function q0(t){let e="\x1B[38;2;200;85;15m",r="\x1B[38;2;200;130;90m";return t.replace(/^(@[^\/]+\/)?(.*?)$/,(i,a,c)=>`${e}${a||""}${r}${c}${e}`+Ee.default.reset(""))}function G0(t){let e=/\r\n|\n|\r/g;return t.replace(e,ss.default.EOL)}var Yc=O("@yarnpkg/fslib"),K0={hooks:{afterAllInstalled:async(t,e)=>{await Vc({immutable:e.immutable},...t.workspaces.map(r=>Yc.npath.fromPortablePath(r.cwd)))}}},W0=K0;return wl(V0);})(); +/*! Bundled license information: + +repeat-string/index.js: + (*! + * repeat-string + * + * Copyright (c) 2014-2015, Jon Schlinkert. + * Licensed under the MIT License. + *) + +is-extglob/index.js: + (*! + * is-extglob + * + * Copyright (c) 2014-2016, Jon Schlinkert. + * Licensed under the MIT License. + *) + +is-glob/index.js: + (*! + * is-glob + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + *) + +is-number/index.js: + (*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + *) + +to-regex-range/index.js: + (*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + *) + +fill-range/index.js: + (*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + *) + +queue-microtask/index.js: + (*! queue-microtask. MIT License. Feross Aboukhadijeh *) + +run-parallel/index.js: + (*! run-parallel. MIT License. Feross Aboukhadijeh *) +*/ +return plugin; +} +}; diff --git a/webapp/.yarn/sdks/eslint/bin/eslint.js b/webapp/.yarn/sdks/eslint/bin/eslint.js new file mode 100755 index 0000000000..e6604ff595 --- /dev/null +++ b/webapp/.yarn/sdks/eslint/bin/eslint.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/bin/eslint.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint/bin/eslint.js your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint/bin/eslint.js`)); diff --git a/webapp/.yarn/sdks/eslint/lib/api.js b/webapp/.yarn/sdks/eslint/lib/api.js new file mode 100644 index 0000000000..8addf97fb2 --- /dev/null +++ b/webapp/.yarn/sdks/eslint/lib/api.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint`)); diff --git a/webapp/.yarn/sdks/eslint/lib/config-api.js b/webapp/.yarn/sdks/eslint/lib/config-api.js new file mode 100644 index 0000000000..e84435de9b --- /dev/null +++ b/webapp/.yarn/sdks/eslint/lib/config-api.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/config + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint/config your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint/config`)); diff --git a/webapp/.yarn/sdks/eslint/lib/types/config-api.d.ts b/webapp/.yarn/sdks/eslint/lib/types/config-api.d.ts new file mode 100644 index 0000000000..174070b0e6 --- /dev/null +++ b/webapp/.yarn/sdks/eslint/lib/types/config-api.d.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/config + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint/config your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint/config`)); diff --git a/webapp/.yarn/sdks/eslint/lib/types/index.d.ts b/webapp/.yarn/sdks/eslint/lib/types/index.d.ts new file mode 100644 index 0000000000..19293d02e4 --- /dev/null +++ b/webapp/.yarn/sdks/eslint/lib/types/index.d.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint`)); diff --git a/webapp/.yarn/sdks/eslint/lib/types/rules.d.ts b/webapp/.yarn/sdks/eslint/lib/types/rules.d.ts new file mode 100644 index 0000000000..8d79c4cc19 --- /dev/null +++ b/webapp/.yarn/sdks/eslint/lib/types/rules.d.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/rules + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint/rules your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint/rules`)); diff --git a/webapp/.yarn/sdks/eslint/lib/types/universal.d.ts b/webapp/.yarn/sdks/eslint/lib/types/universal.d.ts new file mode 100644 index 0000000000..662b3f4fd6 --- /dev/null +++ b/webapp/.yarn/sdks/eslint/lib/types/universal.d.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/universal + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint/universal your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint/universal`)); diff --git a/webapp/.yarn/sdks/eslint/lib/types/use-at-your-own-risk.d.ts b/webapp/.yarn/sdks/eslint/lib/types/use-at-your-own-risk.d.ts new file mode 100644 index 0000000000..2e2ccca283 --- /dev/null +++ b/webapp/.yarn/sdks/eslint/lib/types/use-at-your-own-risk.d.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/use-at-your-own-risk + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint/use-at-your-own-risk your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint/use-at-your-own-risk`)); diff --git a/webapp/.yarn/sdks/eslint/lib/universal.js b/webapp/.yarn/sdks/eslint/lib/universal.js new file mode 100644 index 0000000000..85a8ccbcea --- /dev/null +++ b/webapp/.yarn/sdks/eslint/lib/universal.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/universal + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint/universal your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint/universal`)); diff --git a/webapp/.yarn/sdks/eslint/lib/unsupported-api.js b/webapp/.yarn/sdks/eslint/lib/unsupported-api.js new file mode 100644 index 0000000000..c2b464ce69 --- /dev/null +++ b/webapp/.yarn/sdks/eslint/lib/unsupported-api.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/use-at-your-own-risk + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real eslint/use-at-your-own-risk your application uses +module.exports = wrapWithUserWrapper(absRequire(`eslint/use-at-your-own-risk`)); diff --git a/webapp/.yarn/sdks/eslint/package.json b/webapp/.yarn/sdks/eslint/package.json new file mode 100644 index 0000000000..74487738dc --- /dev/null +++ b/webapp/.yarn/sdks/eslint/package.json @@ -0,0 +1,31 @@ +{ + "name": "eslint", + "version": "9.35.0-sdk", + "main": "./lib/api.js", + "type": "commonjs", + "bin": { + "eslint": "./bin/eslint.js" + }, + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/api.js" + }, + "./config": { + "types": "./lib/types/config-api.d.ts", + "default": "./lib/config-api.js" + }, + "./package.json": "./package.json", + "./use-at-your-own-risk": { + "types": "./lib/types/use-at-your-own-risk.d.ts", + "default": "./lib/unsupported-api.js" + }, + "./rules": { + "types": "./lib/types/rules.d.ts" + }, + "./universal": { + "types": "./lib/types/universal.d.ts", + "default": "./lib/universal.js" + } + } +} diff --git a/webapp/.yarn/sdks/integrations.yml b/webapp/.yarn/sdks/integrations.yml new file mode 100644 index 0000000000..aa9d0d0ad8 --- /dev/null +++ b/webapp/.yarn/sdks/integrations.yml @@ -0,0 +1,5 @@ +# This file is automatically generated by @yarnpkg/sdks. +# Manual changes might be lost! + +integrations: + - vscode diff --git a/webapp/.yarn/sdks/prettier/bin/prettier.cjs b/webapp/.yarn/sdks/prettier/bin/prettier.cjs new file mode 100755 index 0000000000..9a4098f7dc --- /dev/null +++ b/webapp/.yarn/sdks/prettier/bin/prettier.cjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require prettier/bin/prettier.cjs + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real prettier/bin/prettier.cjs your application uses +module.exports = wrapWithUserWrapper(absRequire(`prettier/bin/prettier.cjs`)); diff --git a/webapp/.yarn/sdks/prettier/index.cjs b/webapp/.yarn/sdks/prettier/index.cjs new file mode 100644 index 0000000000..57cb2ab17f --- /dev/null +++ b/webapp/.yarn/sdks/prettier/index.cjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require prettier + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real prettier your application uses +module.exports = wrapWithUserWrapper(absRequire(`prettier`)); diff --git a/webapp/.yarn/sdks/prettier/package.json b/webapp/.yarn/sdks/prettier/package.json new file mode 100644 index 0000000000..1488e98c7d --- /dev/null +++ b/webapp/.yarn/sdks/prettier/package.json @@ -0,0 +1,7 @@ +{ + "name": "prettier", + "version": "3.6.2-sdk", + "main": "./index.cjs", + "type": "commonjs", + "bin": "./bin/prettier.cjs" +} diff --git a/webapp/.yarn/sdks/typescript/bin/tsc b/webapp/.yarn/sdks/typescript/bin/tsc new file mode 100755 index 0000000000..867a7bdfe2 --- /dev/null +++ b/webapp/.yarn/sdks/typescript/bin/tsc @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/bin/tsc + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript/bin/tsc your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`)); diff --git a/webapp/.yarn/sdks/typescript/bin/tsserver b/webapp/.yarn/sdks/typescript/bin/tsserver new file mode 100755 index 0000000000..3fc5aa31cc --- /dev/null +++ b/webapp/.yarn/sdks/typescript/bin/tsserver @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/bin/tsserver + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript/bin/tsserver your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`)); diff --git a/webapp/.yarn/sdks/typescript/lib/tsc.js b/webapp/.yarn/sdks/typescript/lib/tsc.js new file mode 100644 index 0000000000..da411bdba0 --- /dev/null +++ b/webapp/.yarn/sdks/typescript/lib/tsc.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/lib/tsc.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript/lib/tsc.js your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript/lib/tsc.js`)); diff --git a/webapp/.yarn/sdks/typescript/lib/tsserver.js b/webapp/.yarn/sdks/typescript/lib/tsserver.js new file mode 100644 index 0000000000..6249c4675a --- /dev/null +++ b/webapp/.yarn/sdks/typescript/lib/tsserver.js @@ -0,0 +1,248 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/lib/tsserver.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +const moduleWrapper = exports => { + return wrapWithUserWrapper(moduleWrapperFn(exports)); +}; + +const moduleWrapperFn = tsserver => { + if (!process.versions.pnp) { + return tsserver; + } + + const {isAbsolute} = require(`path`); + const pnpApi = require(`pnpapi`); + + const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); + const isPortal = str => str.startsWith("portal:/"); + const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); + + const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { + return `${locator.name}@${locator.reference}`; + })); + + // VSCode sends the zip paths to TS using the "zip://" prefix, that TS + // doesn't understand. This layer makes sure to remove the protocol + // before forwarding it to TS, and to add it back on all returned paths. + + function toEditorPath(str) { + // We add the `zip:` prefix to both `.zip/` paths and virtual paths + if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { + // We also take the opportunity to turn virtual paths into physical ones; + // this makes it much easier to work with workspaces that list peer + // dependencies, since otherwise Ctrl+Click would bring us to the virtual + // file instances instead of the real ones. + // + // We only do this to modules owned by the the dependency tree roots. + // This avoids breaking the resolution when jumping inside a vendor + // with peer dep (otherwise jumping into react-dom would show resolution + // errors on react). + // + const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; + if (resolved) { + const locator = pnpApi.findPackageLocator(resolved); + if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { + str = resolved; + } + } + + str = normalize(str); + + if (str.match(/\.zip\//)) { + switch (hostInfo) { + // Absolute VSCode `Uri.fsPath`s need to start with a slash. + // VSCode only adds it automatically for supported schemes, + // so we have to do it manually for the `zip` scheme. + // The path needs to start with a caret otherwise VSCode doesn't handle the protocol + // + // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 + // + // 2021-10-08: VSCode changed the format in 1.61. + // Before | ^zip:/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + // 2022-04-06: VSCode changed the format in 1.66. + // Before | ^/zip//c:/foo/bar.zip/package.json + // After | ^/zip/c:/foo/bar.zip/package.json + // + // 2022-05-06: VSCode changed the format in 1.68 + // Before | ^/zip/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + case `vscode <1.61`: { + str = `^zip:${str}`; + } break; + + case `vscode <1.66`: { + str = `^/zip/${str}`; + } break; + + case `vscode <1.68`: { + str = `^/zip${str}`; + } break; + + case `vscode`: { + str = `^/zip/${str}`; + } break; + + // To make "go to definition" work, + // We have to resolve the actual file system path from virtual path + // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) + case `coc-nvim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = resolve(`zipfile:${str}`); + } break; + + // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) + // We have to resolve the actual file system path from virtual path, + // everything else is up to neovim + case `neovim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = `zipfile://${str}`; + } break; + + default: { + str = `zip:${str}`; + } break; + } + } else { + str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); + } + } + + return str; + } + + function fromEditorPath(str) { + switch (hostInfo) { + case `coc-nvim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for coc-nvim is in format of //zipfile://.yarn/... + // So in order to convert it back, we use .* to match all the thing + // before `zipfile:` + return process.platform === `win32` + ? str.replace(/^.*zipfile:\//, ``) + : str.replace(/^.*zipfile:/, ``); + } break; + + case `neovim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for neovim is in format of zipfile:////.yarn/... + return str.replace(/^zipfile:\/\//, ``); + } break; + + case `vscode`: + default: { + return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) + } break; + } + } + + // Force enable 'allowLocalPluginLoads' + // TypeScript tries to resolve plugins using a path relative to itself + // which doesn't work when using the global cache + // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 + // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but + // TypeScript already does local loads and if this code is running the user trusts the workspace + // https://github.com/microsoft/vscode/issues/45856 + const ConfiguredProject = tsserver.server.ConfiguredProject; + const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; + ConfiguredProject.prototype.enablePluginsWithOptions = function() { + this.projectService.allowLocalPluginLoads = true; + return originalEnablePluginsWithOptions.apply(this, arguments); + }; + + // And here is the point where we hijack the VSCode <-> TS communications + // by adding ourselves in the middle. We locate everything that looks + // like an absolute path of ours and normalize it. + + const Session = tsserver.server.Session; + const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; + let hostInfo = `unknown`; + + Object.assign(Session.prototype, { + onMessage(/** @type {string | object} */ message) { + const isStringMessage = typeof message === 'string'; + const parsedMessage = isStringMessage ? JSON.parse(message) : message; + + if ( + parsedMessage != null && + typeof parsedMessage === `object` && + parsedMessage.arguments && + typeof parsedMessage.arguments.hostInfo === `string` + ) { + hostInfo = parsedMessage.arguments.hostInfo; + if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { + const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( + // The RegExp from https://semver.org/ but without the caret at the start + /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ + ) ?? []).map(Number) + + if (major === 1) { + if (minor < 61) { + hostInfo += ` <1.61`; + } else if (minor < 66) { + hostInfo += ` <1.66`; + } else if (minor < 68) { + hostInfo += ` <1.68`; + } + } + } + } + + const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { + return typeof value === 'string' ? fromEditorPath(value) : value; + }); + + return originalOnMessage.call( + this, + isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) + ); + }, + + send(/** @type {any} */ msg) { + return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { + return typeof value === `string` ? toEditorPath(value) : value; + }))); + } + }); + + return tsserver; +}; + +const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); +// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. +// Ref https://github.com/microsoft/TypeScript/pull/55326 +if (major > 5 || (major === 5 && minor >= 5)) { + moduleWrapper(absRequire(`typescript`)); +} + +// Defer to the real typescript/lib/tsserver.js your application uses +module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`)); diff --git a/webapp/.yarn/sdks/typescript/lib/tsserverlibrary.js b/webapp/.yarn/sdks/typescript/lib/tsserverlibrary.js new file mode 100644 index 0000000000..0e50e0a2b0 --- /dev/null +++ b/webapp/.yarn/sdks/typescript/lib/tsserverlibrary.js @@ -0,0 +1,248 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/lib/tsserverlibrary.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +const moduleWrapper = exports => { + return wrapWithUserWrapper(moduleWrapperFn(exports)); +}; + +const moduleWrapperFn = tsserver => { + if (!process.versions.pnp) { + return tsserver; + } + + const {isAbsolute} = require(`path`); + const pnpApi = require(`pnpapi`); + + const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); + const isPortal = str => str.startsWith("portal:/"); + const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); + + const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { + return `${locator.name}@${locator.reference}`; + })); + + // VSCode sends the zip paths to TS using the "zip://" prefix, that TS + // doesn't understand. This layer makes sure to remove the protocol + // before forwarding it to TS, and to add it back on all returned paths. + + function toEditorPath(str) { + // We add the `zip:` prefix to both `.zip/` paths and virtual paths + if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { + // We also take the opportunity to turn virtual paths into physical ones; + // this makes it much easier to work with workspaces that list peer + // dependencies, since otherwise Ctrl+Click would bring us to the virtual + // file instances instead of the real ones. + // + // We only do this to modules owned by the the dependency tree roots. + // This avoids breaking the resolution when jumping inside a vendor + // with peer dep (otherwise jumping into react-dom would show resolution + // errors on react). + // + const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; + if (resolved) { + const locator = pnpApi.findPackageLocator(resolved); + if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { + str = resolved; + } + } + + str = normalize(str); + + if (str.match(/\.zip\//)) { + switch (hostInfo) { + // Absolute VSCode `Uri.fsPath`s need to start with a slash. + // VSCode only adds it automatically for supported schemes, + // so we have to do it manually for the `zip` scheme. + // The path needs to start with a caret otherwise VSCode doesn't handle the protocol + // + // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 + // + // 2021-10-08: VSCode changed the format in 1.61. + // Before | ^zip:/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + // 2022-04-06: VSCode changed the format in 1.66. + // Before | ^/zip//c:/foo/bar.zip/package.json + // After | ^/zip/c:/foo/bar.zip/package.json + // + // 2022-05-06: VSCode changed the format in 1.68 + // Before | ^/zip/c:/foo/bar.zip/package.json + // After | ^/zip//c:/foo/bar.zip/package.json + // + case `vscode <1.61`: { + str = `^zip:${str}`; + } break; + + case `vscode <1.66`: { + str = `^/zip/${str}`; + } break; + + case `vscode <1.68`: { + str = `^/zip${str}`; + } break; + + case `vscode`: { + str = `^/zip/${str}`; + } break; + + // To make "go to definition" work, + // We have to resolve the actual file system path from virtual path + // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) + case `coc-nvim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = resolve(`zipfile:${str}`); + } break; + + // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) + // We have to resolve the actual file system path from virtual path, + // everything else is up to neovim + case `neovim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = `zipfile://${str}`; + } break; + + default: { + str = `zip:${str}`; + } break; + } + } else { + str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); + } + } + + return str; + } + + function fromEditorPath(str) { + switch (hostInfo) { + case `coc-nvim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for coc-nvim is in format of //zipfile://.yarn/... + // So in order to convert it back, we use .* to match all the thing + // before `zipfile:` + return process.platform === `win32` + ? str.replace(/^.*zipfile:\//, ``) + : str.replace(/^.*zipfile:/, ``); + } break; + + case `neovim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for neovim is in format of zipfile:////.yarn/... + return str.replace(/^zipfile:\/\//, ``); + } break; + + case `vscode`: + default: { + return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) + } break; + } + } + + // Force enable 'allowLocalPluginLoads' + // TypeScript tries to resolve plugins using a path relative to itself + // which doesn't work when using the global cache + // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 + // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but + // TypeScript already does local loads and if this code is running the user trusts the workspace + // https://github.com/microsoft/vscode/issues/45856 + const ConfiguredProject = tsserver.server.ConfiguredProject; + const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; + ConfiguredProject.prototype.enablePluginsWithOptions = function() { + this.projectService.allowLocalPluginLoads = true; + return originalEnablePluginsWithOptions.apply(this, arguments); + }; + + // And here is the point where we hijack the VSCode <-> TS communications + // by adding ourselves in the middle. We locate everything that looks + // like an absolute path of ours and normalize it. + + const Session = tsserver.server.Session; + const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; + let hostInfo = `unknown`; + + Object.assign(Session.prototype, { + onMessage(/** @type {string | object} */ message) { + const isStringMessage = typeof message === 'string'; + const parsedMessage = isStringMessage ? JSON.parse(message) : message; + + if ( + parsedMessage != null && + typeof parsedMessage === `object` && + parsedMessage.arguments && + typeof parsedMessage.arguments.hostInfo === `string` + ) { + hostInfo = parsedMessage.arguments.hostInfo; + if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { + const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( + // The RegExp from https://semver.org/ but without the caret at the start + /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ + ) ?? []).map(Number) + + if (major === 1) { + if (minor < 61) { + hostInfo += ` <1.61`; + } else if (minor < 66) { + hostInfo += ` <1.66`; + } else if (minor < 68) { + hostInfo += ` <1.68`; + } + } + } + } + + const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { + return typeof value === 'string' ? fromEditorPath(value) : value; + }); + + return originalOnMessage.call( + this, + isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) + ); + }, + + send(/** @type {any} */ msg) { + return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { + return typeof value === `string` ? toEditorPath(value) : value; + }))); + } + }); + + return tsserver; +}; + +const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); +// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. +// Ref https://github.com/microsoft/TypeScript/pull/55326 +if (major > 5 || (major === 5 && minor >= 5)) { + moduleWrapper(absRequire(`typescript`)); +} + +// Defer to the real typescript/lib/tsserverlibrary.js your application uses +module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`)); diff --git a/webapp/.yarn/sdks/typescript/lib/typescript.js b/webapp/.yarn/sdks/typescript/lib/typescript.js new file mode 100644 index 0000000000..7b6cc22079 --- /dev/null +++ b/webapp/.yarn/sdks/typescript/lib/typescript.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, register} = require(`module`); +const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); +const absRequire = createRequire(absPnpApiPath); + +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + +const wrapWithUserWrapper = existsSync(absUserWrapperPath) + ? exports => absRequire(absUserWrapperPath)(exports) + : exports => exports; + +// Defer to the real typescript your application uses +module.exports = wrapWithUserWrapper(absRequire(`typescript`)); diff --git a/webapp/.yarn/sdks/typescript/package.json b/webapp/.yarn/sdks/typescript/package.json new file mode 100644 index 0000000000..aa23045255 --- /dev/null +++ b/webapp/.yarn/sdks/typescript/package.json @@ -0,0 +1,10 @@ +{ + "name": "typescript", + "version": "5.9.2-sdk", + "main": "./lib/typescript.js", + "type": "commonjs", + "bin": { + "tsc": "./bin/tsc", + "tsserver": "./bin/tsserver" + } +} diff --git a/webapp/.yarnrc.yml b/webapp/.yarnrc.yml new file mode 100644 index 0000000000..22876c320f --- /dev/null +++ b/webapp/.yarnrc.yml @@ -0,0 +1,22 @@ +nodeLinker: pnp +enableTelemetry: false +preferReuse: true + +packageExtensions: + "@material/switch@*": + dependencies: + "@material/density": "*" + "@material/touch-target@*": + dependencies: + "@material/base": "*" + vite-multiple-assets@*: + dependencies: + mime-types: "*" + "@typescript-eslint/type-utils@*": + dependencies: + "@typescript-eslint/types": "*" + +plugins: + - checksum: 50414773926286d8c0fc32e9936a799ad23296281046f03674bc33517b597e1ff7856092c3fcf8d5460a29549ef8b3dca3f59a1732b650a02bf523b255d726e7 + path: .yarn/plugins/@yarnpkg/plugin-ts-project-linker.cjs + spec: "https://raw.githubusercontent.com/Wroud/foundation/refs/heads/main/packages/yarn-plugin-ts-project-linker/bundles/%40yarnpkg/plugin-ts-project-linker.js" diff --git a/webapp/common-react/.editorconfig b/webapp/common-react/.editorconfig new file mode 100644 index 0000000000..1ed453a371 --- /dev/null +++ b/webapp/common-react/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{js,json,yml}] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/webapp/common-react/.gitattributes b/webapp/common-react/.gitattributes new file mode 100644 index 0000000000..af3ad12812 --- /dev/null +++ b/webapp/common-react/.gitattributes @@ -0,0 +1,4 @@ +/.yarn/** linguist-vendored +/.yarn/releases/* binary +/.yarn/plugins/**/* binary +/.pnp.* binary linguist-generated diff --git a/webapp/common-react/.gitignore b/webapp/common-react/.gitignore new file mode 100644 index 0000000000..f99b59bcf3 --- /dev/null +++ b/webapp/common-react/.gitignore @@ -0,0 +1,21 @@ +# artifacts +lib +out +dist +node_modules + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +!.yarn/sdks/** + +# Swap the comments on the following lines if you wish to use zero-installs +# In that case, don't forget to run `yarn config set enableGlobalCache false`! +# Documentation here: https://yarnpkg.com/features/caching#zero-installs + +#!.yarn/cache +.pnp.* diff --git a/webapp/common-react/.prettierrc.mjs b/webapp/common-react/.prettierrc.mjs new file mode 100644 index 0000000000..de21a410e9 --- /dev/null +++ b/webapp/common-react/.prettierrc.mjs @@ -0,0 +1,10 @@ +import defaultDbeaverConfig from '@dbeaver/prettier-config'; + +/** + * @type {import("prettier").Config} + */ +const config = { + ...defaultDbeaverConfig, +}; + +export default config; diff --git a/webapp/common-react/.vscode/extensions.json b/webapp/common-react/.vscode/extensions.json new file mode 100644 index 0000000000..daaa5ee2ec --- /dev/null +++ b/webapp/common-react/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "arcanis.vscode-zipfs", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] +} diff --git a/webapp/common-react/.vscode/settings.json b/webapp/common-react/.vscode/settings.json new file mode 100644 index 0000000000..3e58133924 --- /dev/null +++ b/webapp/common-react/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "search.exclude": { + "**/.yarn": true, + "**/.pnp.*": true + }, + "eslint.nodePath": "../.yarn/sdks", + "prettier.prettierPath": "../.yarn/sdks/prettier/index.cjs", + "typescript.tsdk": "../.yarn/sdks/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} diff --git a/webapp/common-react/.yarn/plugins/@yarnpkg/plugin-ts-project-linker.cjs b/webapp/common-react/.yarn/plugins/@yarnpkg/plugin-ts-project-linker.cjs new file mode 100644 index 0000000000..39e5a79818 --- /dev/null +++ b/webapp/common-react/.yarn/plugins/@yarnpkg/plugin-ts-project-linker.cjs @@ -0,0 +1,74 @@ +/* eslint-disable */ +//prettier-ignore +module.exports = { +name: "@yarnpkg/plugin-ts-project-linker", +factory: function (require) { +var plugin=(()=>{var vl=Object.create;var kt=Object.defineProperty;var Sl=Object.getOwnPropertyDescriptor;var Al=Object.getOwnPropertyNames;var Cl=Object.getPrototypeOf,bl=Object.prototype.hasOwnProperty;var O=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,r)=>(typeof require<"u"?require:e)[r]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var _=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),Fl=(t,e)=>{for(var r in e)kt(t,r,{get:e[r],enumerable:!0})},as=(t,e,r,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of Al(e))!bl.call(t,a)&&a!==r&&kt(t,a,{get:()=>e[a],enumerable:!(i=Sl(e,a))||i.enumerable});return t};var ne=(t,e,r)=>(r=t!=null?vl(Cl(t)):{},as(e||!t||!t.__esModule?kt(r,"default",{value:t,enumerable:!0}):r,t)),wl=t=>as(kt({},"__esModule",{value:!0}),t);var ls=_((Z0,Fr)=>{var Bt=process||{},us=Bt.argv||[],Tt=Bt.env||{},_l=!(Tt.NO_COLOR||us.includes("--no-color"))&&(!!Tt.FORCE_COLOR||us.includes("--color")||Bt.platform==="win32"||(Bt.stdout||{}).isTTY&&Tt.TERM!=="dumb"||!!Tt.CI),kl=(t,e,r=t)=>i=>{let a=""+i,c=a.indexOf(e,t.length);return~c?t+Tl(a,e,r,c)+e:t+a+e},Tl=(t,e,r,i)=>{let a="",c=0;do a+=t.substring(c,i)+r,c=i+e.length,i=t.indexOf(e,c);while(~i);return a+t.substring(c)},cs=(t=_l)=>{let e=t?kl:()=>String;return{isColorSupported:t,reset:e("\x1B[0m","\x1B[0m"),bold:e("\x1B[1m","\x1B[22m","\x1B[22m\x1B[1m"),dim:e("\x1B[2m","\x1B[22m","\x1B[22m\x1B[2m"),italic:e("\x1B[3m","\x1B[23m"),underline:e("\x1B[4m","\x1B[24m"),inverse:e("\x1B[7m","\x1B[27m"),hidden:e("\x1B[8m","\x1B[28m"),strikethrough:e("\x1B[9m","\x1B[29m"),black:e("\x1B[30m","\x1B[39m"),red:e("\x1B[31m","\x1B[39m"),green:e("\x1B[32m","\x1B[39m"),yellow:e("\x1B[33m","\x1B[39m"),blue:e("\x1B[34m","\x1B[39m"),magenta:e("\x1B[35m","\x1B[39m"),cyan:e("\x1B[36m","\x1B[39m"),white:e("\x1B[37m","\x1B[39m"),gray:e("\x1B[90m","\x1B[39m"),bgBlack:e("\x1B[40m","\x1B[49m"),bgRed:e("\x1B[41m","\x1B[49m"),bgGreen:e("\x1B[42m","\x1B[49m"),bgYellow:e("\x1B[43m","\x1B[49m"),bgBlue:e("\x1B[44m","\x1B[49m"),bgMagenta:e("\x1B[45m","\x1B[49m"),bgCyan:e("\x1B[46m","\x1B[49m"),bgWhite:e("\x1B[47m","\x1B[49m"),blackBright:e("\x1B[90m","\x1B[39m"),redBright:e("\x1B[91m","\x1B[39m"),greenBright:e("\x1B[92m","\x1B[39m"),yellowBright:e("\x1B[93m","\x1B[39m"),blueBright:e("\x1B[94m","\x1B[39m"),magentaBright:e("\x1B[95m","\x1B[39m"),cyanBright:e("\x1B[96m","\x1B[39m"),whiteBright:e("\x1B[97m","\x1B[39m"),bgBlackBright:e("\x1B[100m","\x1B[49m"),bgRedBright:e("\x1B[101m","\x1B[49m"),bgGreenBright:e("\x1B[102m","\x1B[49m"),bgYellowBright:e("\x1B[103m","\x1B[49m"),bgBlueBright:e("\x1B[104m","\x1B[49m"),bgMagentaBright:e("\x1B[105m","\x1B[49m"),bgCyanBright:e("\x1B[106m","\x1B[49m"),bgWhiteBright:e("\x1B[107m","\x1B[49m")}};Fr.exports=cs();Fr.exports.createColors=cs});var hs=_((dt,wr)=>{(function(e,r){typeof dt=="object"&&typeof wr=="object"?wr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof dt=="object"?dt.esprima=r():e.esprima=r()})(dt,function(){return function(t){var e={};function r(i){if(e[i])return e[i].exports;var a=e[i]={exports:{},id:i,loaded:!1};return t[i].call(a.exports,a,a.exports,r),a.loaded=!0,a.exports}return r.m=t,r.c=e,r.p="",r(0)}([function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(1),a=r(3),c=r(8),u=r(15);function x(l,n,s){var o=null,f=function(P,N){s&&s(P,N),o&&o.visit(P,N)},E=typeof s=="function"?f:null,g=!1;if(n){g=typeof n.comment=="boolean"&&n.comment;var v=typeof n.attachComment=="boolean"&&n.attachComment;(g||v)&&(o=new i.CommentHandler,o.attach=v,n.comment=!0,E=f)}var F=!1;n&&typeof n.sourceType=="string"&&(F=n.sourceType==="module");var A;n&&typeof n.jsx=="boolean"&&n.jsx?A=new a.JSXParser(l,n,E):A=new c.Parser(l,n,E);var T=F?A.parseModule():A.parseScript(),k=T;return g&&o&&(k.comments=o.comments),A.config.tokens&&(k.tokens=A.tokens),A.config.tolerant&&(k.errors=A.errorHandler.errors),k}e.parse=x;function d(l,n,s){var o=n||{};return o.sourceType="module",x(l,o,s)}e.parseModule=d;function p(l,n,s){var o=n||{};return o.sourceType="script",x(l,o,s)}e.parseScript=p;function h(l,n,s){var o=new u.Tokenizer(l,n),f;f=[];try{for(;;){var E=o.getNextToken();if(!E)break;s&&(E=s(E)),f.push(E)}}catch(g){o.errorHandler.tolerate(g)}return o.errorHandler.tolerant&&(f.errors=o.errors()),f}e.tokenize=h;var m=r(2);e.Syntax=m.Syntax,e.version="4.0.1"},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(2),a=function(){function c(){this.attach=!1,this.comments=[],this.stack=[],this.leading=[],this.trailing=[]}return c.prototype.insertInnerComments=function(u,x){if(u.type===i.Syntax.BlockStatement&&u.body.length===0){for(var d=[],p=this.leading.length-1;p>=0;--p){var h=this.leading[p];x.end.offset>=h.start&&(d.unshift(h.comment),this.leading.splice(p,1),this.trailing.splice(p,1))}d.length&&(u.innerComments=d)}},c.prototype.findTrailingComments=function(u){var x=[];if(this.trailing.length>0){for(var d=this.trailing.length-1;d>=0;--d){var p=this.trailing[d];p.start>=u.end.offset&&x.unshift(p.comment)}return this.trailing.length=0,x}var h=this.stack[this.stack.length-1];if(h&&h.node.trailingComments){var m=h.node.trailingComments[0];m&&m.range[0]>=u.end.offset&&(x=h.node.trailingComments,delete h.node.trailingComments)}return x},c.prototype.findLeadingComments=function(u){for(var x=[],d;this.stack.length>0;){var p=this.stack[this.stack.length-1];if(p&&p.start>=u.start.offset)d=p.node,this.stack.pop();else break}if(d){for(var h=d.leadingComments?d.leadingComments.length:0,m=h-1;m>=0;--m){var l=d.leadingComments[m];l.range[1]<=u.start.offset&&(x.unshift(l),d.leadingComments.splice(m,1))}return d.leadingComments&&d.leadingComments.length===0&&delete d.leadingComments,x}for(var m=this.leading.length-1;m>=0;--m){var p=this.leading[m];p.start<=u.start.offset&&(x.unshift(p.comment),this.leading.splice(m,1))}return x},c.prototype.visitNode=function(u,x){if(!(u.type===i.Syntax.Program&&u.body.length>0)){this.insertInnerComments(u,x);var d=this.findTrailingComments(x),p=this.findLeadingComments(x);p.length>0&&(u.leadingComments=p),d.length>0&&(u.trailingComments=d),this.stack.push({node:u,start:x.start.offset})}},c.prototype.visitComment=function(u,x){var d=u.type[0]==="L"?"Line":"Block",p={type:d,value:u.value};if(u.range&&(p.range=u.range),u.loc&&(p.loc=u.loc),this.comments.push(p),this.attach){var h={comment:{type:d,value:u.value,range:[x.start.offset,x.end.offset]},start:x.start.offset};u.loc&&(h.comment.loc=u.loc),u.type=d,this.leading.push(h),this.trailing.push(h)}},c.prototype.visit=function(u,x){u.type==="LineComment"?this.visitComment(u,x):u.type==="BlockComment"?this.visitComment(u,x):this.attach&&this.visitNode(u,x)},c}();e.CommentHandler=a},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Syntax={AssignmentExpression:"AssignmentExpression",AssignmentPattern:"AssignmentPattern",ArrayExpression:"ArrayExpression",ArrayPattern:"ArrayPattern",ArrowFunctionExpression:"ArrowFunctionExpression",AwaitExpression:"AwaitExpression",BlockStatement:"BlockStatement",BinaryExpression:"BinaryExpression",BreakStatement:"BreakStatement",CallExpression:"CallExpression",CatchClause:"CatchClause",ClassBody:"ClassBody",ClassDeclaration:"ClassDeclaration",ClassExpression:"ClassExpression",ConditionalExpression:"ConditionalExpression",ContinueStatement:"ContinueStatement",DoWhileStatement:"DoWhileStatement",DebuggerStatement:"DebuggerStatement",EmptyStatement:"EmptyStatement",ExportAllDeclaration:"ExportAllDeclaration",ExportDefaultDeclaration:"ExportDefaultDeclaration",ExportNamedDeclaration:"ExportNamedDeclaration",ExportSpecifier:"ExportSpecifier",ExpressionStatement:"ExpressionStatement",ForStatement:"ForStatement",ForOfStatement:"ForOfStatement",ForInStatement:"ForInStatement",FunctionDeclaration:"FunctionDeclaration",FunctionExpression:"FunctionExpression",Identifier:"Identifier",IfStatement:"IfStatement",ImportDeclaration:"ImportDeclaration",ImportDefaultSpecifier:"ImportDefaultSpecifier",ImportNamespaceSpecifier:"ImportNamespaceSpecifier",ImportSpecifier:"ImportSpecifier",Literal:"Literal",LabeledStatement:"LabeledStatement",LogicalExpression:"LogicalExpression",MemberExpression:"MemberExpression",MetaProperty:"MetaProperty",MethodDefinition:"MethodDefinition",NewExpression:"NewExpression",ObjectExpression:"ObjectExpression",ObjectPattern:"ObjectPattern",Program:"Program",Property:"Property",RestElement:"RestElement",ReturnStatement:"ReturnStatement",SequenceExpression:"SequenceExpression",SpreadElement:"SpreadElement",Super:"Super",SwitchCase:"SwitchCase",SwitchStatement:"SwitchStatement",TaggedTemplateExpression:"TaggedTemplateExpression",TemplateElement:"TemplateElement",TemplateLiteral:"TemplateLiteral",ThisExpression:"ThisExpression",ThrowStatement:"ThrowStatement",TryStatement:"TryStatement",UnaryExpression:"UnaryExpression",UpdateExpression:"UpdateExpression",VariableDeclaration:"VariableDeclaration",VariableDeclarator:"VariableDeclarator",WhileStatement:"WhileStatement",WithStatement:"WithStatement",YieldExpression:"YieldExpression"}},function(t,e,r){"use strict";var i=this&&this.__extends||function(){var n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(s,o){s.__proto__=o}||function(s,o){for(var f in o)o.hasOwnProperty(f)&&(s[f]=o[f])};return function(s,o){n(s,o);function f(){this.constructor=s}s.prototype=o===null?Object.create(o):(f.prototype=o.prototype,new f)}}();Object.defineProperty(e,"__esModule",{value:!0});var a=r(4),c=r(5),u=r(6),x=r(7),d=r(8),p=r(13),h=r(14);p.TokenName[100]="JSXIdentifier",p.TokenName[101]="JSXText";function m(n){var s;switch(n.type){case u.JSXSyntax.JSXIdentifier:var o=n;s=o.name;break;case u.JSXSyntax.JSXNamespacedName:var f=n;s=m(f.namespace)+":"+m(f.name);break;case u.JSXSyntax.JSXMemberExpression:var E=n;s=m(E.object)+"."+m(E.property);break;default:break}return s}var l=function(n){i(s,n);function s(o,f,E){return n.call(this,o,f,E)||this}return s.prototype.parsePrimaryExpression=function(){return this.match("<")?this.parseJSXRoot():n.prototype.parsePrimaryExpression.call(this)},s.prototype.startJSX=function(){this.scanner.index=this.startMarker.index,this.scanner.lineNumber=this.startMarker.line,this.scanner.lineStart=this.startMarker.index-this.startMarker.column},s.prototype.finishJSX=function(){this.nextToken()},s.prototype.reenterJSX=function(){this.startJSX(),this.expectJSX("}"),this.config.tokens&&this.tokens.pop()},s.prototype.createJSXNode=function(){return this.collectComments(),{index:this.scanner.index,line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart}},s.prototype.createJSXChildNode=function(){return{index:this.scanner.index,line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart}},s.prototype.scanXHTMLEntity=function(o){for(var f="&",E=!0,g=!1,v=!1,F=!1;!this.scanner.eof()&&E&&!g;){var A=this.scanner.source[this.scanner.index];if(A===o)break;if(g=A===";",f+=A,++this.scanner.index,!g)switch(f.length){case 2:v=A==="#";break;case 3:v&&(F=A==="x",E=F||a.Character.isDecimalDigit(A.charCodeAt(0)),v=v&&!F);break;default:E=E&&!(v&&!a.Character.isDecimalDigit(A.charCodeAt(0))),E=E&&!(F&&!a.Character.isHexDigit(A.charCodeAt(0)));break}}if(E&&g&&f.length>2){var T=f.substr(1,f.length-2);v&&T.length>1?f=String.fromCharCode(parseInt(T.substr(1),10)):F&&T.length>2?f=String.fromCharCode(parseInt("0"+T.substr(1),16)):!v&&!F&&h.XHTMLEntities[T]&&(f=h.XHTMLEntities[T])}return f},s.prototype.lexJSX=function(){var o=this.scanner.source.charCodeAt(this.scanner.index);if(o===60||o===62||o===47||o===58||o===61||o===123||o===125){var f=this.scanner.source[this.scanner.index++];return{type:7,value:f,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:this.scanner.index-1,end:this.scanner.index}}if(o===34||o===39){for(var E=this.scanner.index,g=this.scanner.source[this.scanner.index++],v="";!this.scanner.eof();){var F=this.scanner.source[this.scanner.index++];if(F===g)break;F==="&"?v+=this.scanXHTMLEntity(g):v+=F}return{type:8,value:v,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:E,end:this.scanner.index}}if(o===46){var A=this.scanner.source.charCodeAt(this.scanner.index+1),T=this.scanner.source.charCodeAt(this.scanner.index+2),f=A===46&&T===46?"...":".",E=this.scanner.index;return this.scanner.index+=f.length,{type:7,value:f,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:E,end:this.scanner.index}}if(o===96)return{type:10,value:"",lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:this.scanner.index,end:this.scanner.index};if(a.Character.isIdentifierStart(o)&&o!==92){var E=this.scanner.index;for(++this.scanner.index;!this.scanner.eof();){var F=this.scanner.source.charCodeAt(this.scanner.index);if(a.Character.isIdentifierPart(F)&&F!==92)++this.scanner.index;else if(F===45)++this.scanner.index;else break}var k=this.scanner.source.slice(E,this.scanner.index);return{type:100,value:k,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:E,end:this.scanner.index}}return this.scanner.lex()},s.prototype.nextJSXToken=function(){this.collectComments(),this.startMarker.index=this.scanner.index,this.startMarker.line=this.scanner.lineNumber,this.startMarker.column=this.scanner.index-this.scanner.lineStart;var o=this.lexJSX();return this.lastMarker.index=this.scanner.index,this.lastMarker.line=this.scanner.lineNumber,this.lastMarker.column=this.scanner.index-this.scanner.lineStart,this.config.tokens&&this.tokens.push(this.convertToken(o)),o},s.prototype.nextJSXText=function(){this.startMarker.index=this.scanner.index,this.startMarker.line=this.scanner.lineNumber,this.startMarker.column=this.scanner.index-this.scanner.lineStart;for(var o=this.scanner.index,f="";!this.scanner.eof();){var E=this.scanner.source[this.scanner.index];if(E==="{"||E==="<")break;++this.scanner.index,f+=E,a.Character.isLineTerminator(E.charCodeAt(0))&&(++this.scanner.lineNumber,E==="\r"&&this.scanner.source[this.scanner.index]===` +`&&++this.scanner.index,this.scanner.lineStart=this.scanner.index)}this.lastMarker.index=this.scanner.index,this.lastMarker.line=this.scanner.lineNumber,this.lastMarker.column=this.scanner.index-this.scanner.lineStart;var g={type:101,value:f,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:o,end:this.scanner.index};return f.length>0&&this.config.tokens&&this.tokens.push(this.convertToken(g)),g},s.prototype.peekJSXToken=function(){var o=this.scanner.saveState();this.scanner.scanComments();var f=this.lexJSX();return this.scanner.restoreState(o),f},s.prototype.expectJSX=function(o){var f=this.nextJSXToken();(f.type!==7||f.value!==o)&&this.throwUnexpectedToken(f)},s.prototype.matchJSX=function(o){var f=this.peekJSXToken();return f.type===7&&f.value===o},s.prototype.parseJSXIdentifier=function(){var o=this.createJSXNode(),f=this.nextJSXToken();return f.type!==100&&this.throwUnexpectedToken(f),this.finalize(o,new c.JSXIdentifier(f.value))},s.prototype.parseJSXElementName=function(){var o=this.createJSXNode(),f=this.parseJSXIdentifier();if(this.matchJSX(":")){var E=f;this.expectJSX(":");var g=this.parseJSXIdentifier();f=this.finalize(o,new c.JSXNamespacedName(E,g))}else if(this.matchJSX("."))for(;this.matchJSX(".");){var v=f;this.expectJSX(".");var F=this.parseJSXIdentifier();f=this.finalize(o,new c.JSXMemberExpression(v,F))}return f},s.prototype.parseJSXAttributeName=function(){var o=this.createJSXNode(),f,E=this.parseJSXIdentifier();if(this.matchJSX(":")){var g=E;this.expectJSX(":");var v=this.parseJSXIdentifier();f=this.finalize(o,new c.JSXNamespacedName(g,v))}else f=E;return f},s.prototype.parseJSXStringLiteralAttribute=function(){var o=this.createJSXNode(),f=this.nextJSXToken();f.type!==8&&this.throwUnexpectedToken(f);var E=this.getTokenRaw(f);return this.finalize(o,new x.Literal(f.value,E))},s.prototype.parseJSXExpressionAttribute=function(){var o=this.createJSXNode();this.expectJSX("{"),this.finishJSX(),this.match("}")&&this.tolerateError("JSX attributes must only be assigned a non-empty expression");var f=this.parseAssignmentExpression();return this.reenterJSX(),this.finalize(o,new c.JSXExpressionContainer(f))},s.prototype.parseJSXAttributeValue=function(){return this.matchJSX("{")?this.parseJSXExpressionAttribute():this.matchJSX("<")?this.parseJSXElement():this.parseJSXStringLiteralAttribute()},s.prototype.parseJSXNameValueAttribute=function(){var o=this.createJSXNode(),f=this.parseJSXAttributeName(),E=null;return this.matchJSX("=")&&(this.expectJSX("="),E=this.parseJSXAttributeValue()),this.finalize(o,new c.JSXAttribute(f,E))},s.prototype.parseJSXSpreadAttribute=function(){var o=this.createJSXNode();this.expectJSX("{"),this.expectJSX("..."),this.finishJSX();var f=this.parseAssignmentExpression();return this.reenterJSX(),this.finalize(o,new c.JSXSpreadAttribute(f))},s.prototype.parseJSXAttributes=function(){for(var o=[];!this.matchJSX("/")&&!this.matchJSX(">");){var f=this.matchJSX("{")?this.parseJSXSpreadAttribute():this.parseJSXNameValueAttribute();o.push(f)}return o},s.prototype.parseJSXOpeningElement=function(){var o=this.createJSXNode();this.expectJSX("<");var f=this.parseJSXElementName(),E=this.parseJSXAttributes(),g=this.matchJSX("/");return g&&this.expectJSX("/"),this.expectJSX(">"),this.finalize(o,new c.JSXOpeningElement(f,g,E))},s.prototype.parseJSXBoundaryElement=function(){var o=this.createJSXNode();if(this.expectJSX("<"),this.matchJSX("/")){this.expectJSX("/");var f=this.parseJSXElementName();return this.expectJSX(">"),this.finalize(o,new c.JSXClosingElement(f))}var E=this.parseJSXElementName(),g=this.parseJSXAttributes(),v=this.matchJSX("/");return v&&this.expectJSX("/"),this.expectJSX(">"),this.finalize(o,new c.JSXOpeningElement(E,v,g))},s.prototype.parseJSXEmptyExpression=function(){var o=this.createJSXChildNode();return this.collectComments(),this.lastMarker.index=this.scanner.index,this.lastMarker.line=this.scanner.lineNumber,this.lastMarker.column=this.scanner.index-this.scanner.lineStart,this.finalize(o,new c.JSXEmptyExpression)},s.prototype.parseJSXExpressionContainer=function(){var o=this.createJSXNode();this.expectJSX("{");var f;return this.matchJSX("}")?(f=this.parseJSXEmptyExpression(),this.expectJSX("}")):(this.finishJSX(),f=this.parseAssignmentExpression(),this.reenterJSX()),this.finalize(o,new c.JSXExpressionContainer(f))},s.prototype.parseJSXChildren=function(){for(var o=[];!this.scanner.eof();){var f=this.createJSXChildNode(),E=this.nextJSXText();if(E.start0){var F=this.finalize(o.node,new c.JSXElement(o.opening,o.children,o.closing));o=f[f.length-1],o.children.push(F),f.pop()}else break}}return o},s.prototype.parseJSXElement=function(){var o=this.createJSXNode(),f=this.parseJSXOpeningElement(),E=[],g=null;if(!f.selfClosing){var v=this.parseComplexJSXElement({node:o,opening:f,closing:g,children:E});E=v.children,g=v.closing}return this.finalize(o,new c.JSXElement(f,E,g))},s.prototype.parseJSXRoot=function(){this.config.tokens&&this.tokens.pop(),this.startJSX();var o=this.parseJSXElement();return this.finishJSX(),o},s.prototype.isStartOfExpression=function(){return n.prototype.isStartOfExpression.call(this)||this.match("<")},s}(d.Parser);e.JSXParser=l},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r={NonAsciiIdentifierStart:/[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309B-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]/,NonAsciiIdentifierPart:/[\xAA\xB5\xB7\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B4\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1369-\u1371\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDEC0-\uDEF8]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/};e.Character={fromCodePoint:function(i){return i<65536?String.fromCharCode(i):String.fromCharCode(55296+(i-65536>>10))+String.fromCharCode(56320+(i-65536&1023))},isWhiteSpace:function(i){return i===32||i===9||i===11||i===12||i===160||i>=5760&&[5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8239,8287,12288,65279].indexOf(i)>=0},isLineTerminator:function(i){return i===10||i===13||i===8232||i===8233},isIdentifierStart:function(i){return i===36||i===95||i>=65&&i<=90||i>=97&&i<=122||i===92||i>=128&&r.NonAsciiIdentifierStart.test(e.Character.fromCodePoint(i))},isIdentifierPart:function(i){return i===36||i===95||i>=65&&i<=90||i>=97&&i<=122||i>=48&&i<=57||i===92||i>=128&&r.NonAsciiIdentifierPart.test(e.Character.fromCodePoint(i))},isDecimalDigit:function(i){return i>=48&&i<=57},isHexDigit:function(i){return i>=48&&i<=57||i>=65&&i<=70||i>=97&&i<=102},isOctalDigit:function(i){return i>=48&&i<=55}}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(6),a=function(){function o(f){this.type=i.JSXSyntax.JSXClosingElement,this.name=f}return o}();e.JSXClosingElement=a;var c=function(){function o(f,E,g){this.type=i.JSXSyntax.JSXElement,this.openingElement=f,this.children=E,this.closingElement=g}return o}();e.JSXElement=c;var u=function(){function o(){this.type=i.JSXSyntax.JSXEmptyExpression}return o}();e.JSXEmptyExpression=u;var x=function(){function o(f){this.type=i.JSXSyntax.JSXExpressionContainer,this.expression=f}return o}();e.JSXExpressionContainer=x;var d=function(){function o(f){this.type=i.JSXSyntax.JSXIdentifier,this.name=f}return o}();e.JSXIdentifier=d;var p=function(){function o(f,E){this.type=i.JSXSyntax.JSXMemberExpression,this.object=f,this.property=E}return o}();e.JSXMemberExpression=p;var h=function(){function o(f,E){this.type=i.JSXSyntax.JSXAttribute,this.name=f,this.value=E}return o}();e.JSXAttribute=h;var m=function(){function o(f,E){this.type=i.JSXSyntax.JSXNamespacedName,this.namespace=f,this.name=E}return o}();e.JSXNamespacedName=m;var l=function(){function o(f,E,g){this.type=i.JSXSyntax.JSXOpeningElement,this.name=f,this.selfClosing=E,this.attributes=g}return o}();e.JSXOpeningElement=l;var n=function(){function o(f){this.type=i.JSXSyntax.JSXSpreadAttribute,this.argument=f}return o}();e.JSXSpreadAttribute=n;var s=function(){function o(f,E){this.type=i.JSXSyntax.JSXText,this.value=f,this.raw=E}return o}();e.JSXText=s},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.JSXSyntax={JSXAttribute:"JSXAttribute",JSXClosingElement:"JSXClosingElement",JSXElement:"JSXElement",JSXEmptyExpression:"JSXEmptyExpression",JSXExpressionContainer:"JSXExpressionContainer",JSXIdentifier:"JSXIdentifier",JSXMemberExpression:"JSXMemberExpression",JSXNamespacedName:"JSXNamespacedName",JSXOpeningElement:"JSXOpeningElement",JSXSpreadAttribute:"JSXSpreadAttribute",JSXText:"JSXText"}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(2),a=function(){function D(y){this.type=i.Syntax.ArrayExpression,this.elements=y}return D}();e.ArrayExpression=a;var c=function(){function D(y){this.type=i.Syntax.ArrayPattern,this.elements=y}return D}();e.ArrayPattern=c;var u=function(){function D(y,w,M){this.type=i.Syntax.ArrowFunctionExpression,this.id=null,this.params=y,this.body=w,this.generator=!1,this.expression=M,this.async=!1}return D}();e.ArrowFunctionExpression=u;var x=function(){function D(y,w,M){this.type=i.Syntax.AssignmentExpression,this.operator=y,this.left=w,this.right=M}return D}();e.AssignmentExpression=x;var d=function(){function D(y,w){this.type=i.Syntax.AssignmentPattern,this.left=y,this.right=w}return D}();e.AssignmentPattern=d;var p=function(){function D(y,w,M){this.type=i.Syntax.ArrowFunctionExpression,this.id=null,this.params=y,this.body=w,this.generator=!1,this.expression=M,this.async=!0}return D}();e.AsyncArrowFunctionExpression=p;var h=function(){function D(y,w,M){this.type=i.Syntax.FunctionDeclaration,this.id=y,this.params=w,this.body=M,this.generator=!1,this.expression=!1,this.async=!0}return D}();e.AsyncFunctionDeclaration=h;var m=function(){function D(y,w,M){this.type=i.Syntax.FunctionExpression,this.id=y,this.params=w,this.body=M,this.generator=!1,this.expression=!1,this.async=!0}return D}();e.AsyncFunctionExpression=m;var l=function(){function D(y){this.type=i.Syntax.AwaitExpression,this.argument=y}return D}();e.AwaitExpression=l;var n=function(){function D(y,w,M){var ae=y==="||"||y==="&&";this.type=ae?i.Syntax.LogicalExpression:i.Syntax.BinaryExpression,this.operator=y,this.left=w,this.right=M}return D}();e.BinaryExpression=n;var s=function(){function D(y){this.type=i.Syntax.BlockStatement,this.body=y}return D}();e.BlockStatement=s;var o=function(){function D(y){this.type=i.Syntax.BreakStatement,this.label=y}return D}();e.BreakStatement=o;var f=function(){function D(y,w){this.type=i.Syntax.CallExpression,this.callee=y,this.arguments=w}return D}();e.CallExpression=f;var E=function(){function D(y,w){this.type=i.Syntax.CatchClause,this.param=y,this.body=w}return D}();e.CatchClause=E;var g=function(){function D(y){this.type=i.Syntax.ClassBody,this.body=y}return D}();e.ClassBody=g;var v=function(){function D(y,w,M){this.type=i.Syntax.ClassDeclaration,this.id=y,this.superClass=w,this.body=M}return D}();e.ClassDeclaration=v;var F=function(){function D(y,w,M){this.type=i.Syntax.ClassExpression,this.id=y,this.superClass=w,this.body=M}return D}();e.ClassExpression=F;var A=function(){function D(y,w){this.type=i.Syntax.MemberExpression,this.computed=!0,this.object=y,this.property=w}return D}();e.ComputedMemberExpression=A;var T=function(){function D(y,w,M){this.type=i.Syntax.ConditionalExpression,this.test=y,this.consequent=w,this.alternate=M}return D}();e.ConditionalExpression=T;var k=function(){function D(y){this.type=i.Syntax.ContinueStatement,this.label=y}return D}();e.ContinueStatement=k;var P=function(){function D(){this.type=i.Syntax.DebuggerStatement}return D}();e.DebuggerStatement=P;var N=function(){function D(y,w){this.type=i.Syntax.ExpressionStatement,this.expression=y,this.directive=w}return D}();e.Directive=N;var U=function(){function D(y,w){this.type=i.Syntax.DoWhileStatement,this.body=y,this.test=w}return D}();e.DoWhileStatement=U;var S=function(){function D(){this.type=i.Syntax.EmptyStatement}return D}();e.EmptyStatement=S;var j=function(){function D(y){this.type=i.Syntax.ExportAllDeclaration,this.source=y}return D}();e.ExportAllDeclaration=j;var J=function(){function D(y){this.type=i.Syntax.ExportDefaultDeclaration,this.declaration=y}return D}();e.ExportDefaultDeclaration=J;var ye=function(){function D(y,w,M){this.type=i.Syntax.ExportNamedDeclaration,this.declaration=y,this.specifiers=w,this.source=M}return D}();e.ExportNamedDeclaration=ye;var C=function(){function D(y,w){this.type=i.Syntax.ExportSpecifier,this.exported=w,this.local=y}return D}();e.ExportSpecifier=C;var b=function(){function D(y){this.type=i.Syntax.ExpressionStatement,this.expression=y}return D}();e.ExpressionStatement=b;var Y=function(){function D(y,w,M){this.type=i.Syntax.ForInStatement,this.left=y,this.right=w,this.body=M,this.each=!1}return D}();e.ForInStatement=Y;var $=function(){function D(y,w,M){this.type=i.Syntax.ForOfStatement,this.left=y,this.right=w,this.body=M}return D}();e.ForOfStatement=$;var he=function(){function D(y,w,M,ae){this.type=i.Syntax.ForStatement,this.init=y,this.test=w,this.update=M,this.body=ae}return D}();e.ForStatement=he;var fe=function(){function D(y,w,M,ae){this.type=i.Syntax.FunctionDeclaration,this.id=y,this.params=w,this.body=M,this.generator=ae,this.expression=!1,this.async=!1}return D}();e.FunctionDeclaration=fe;var ie=function(){function D(y,w,M,ae){this.type=i.Syntax.FunctionExpression,this.id=y,this.params=w,this.body=M,this.generator=ae,this.expression=!1,this.async=!1}return D}();e.FunctionExpression=ie;var Ye=function(){function D(y){this.type=i.Syntax.Identifier,this.name=y}return D}();e.Identifier=Ye;var Ar=function(){function D(y,w,M){this.type=i.Syntax.IfStatement,this.test=y,this.consequent=w,this.alternate=M}return D}();e.IfStatement=Ar;var Qe=function(){function D(y,w){this.type=i.Syntax.ImportDeclaration,this.specifiers=y,this.source=w}return D}();e.ImportDeclaration=Qe;var we=function(){function D(y){this.type=i.Syntax.ImportDefaultSpecifier,this.local=y}return D}();e.ImportDefaultSpecifier=we;var X=function(){function D(y){this.type=i.Syntax.ImportNamespaceSpecifier,this.local=y}return D}();e.ImportNamespaceSpecifier=X;var Ze=function(){function D(y,w){this.type=i.Syntax.ImportSpecifier,this.local=y,this.imported=w}return D}();e.ImportSpecifier=Ze;var Cr=function(){function D(y,w){this.type=i.Syntax.LabeledStatement,this.label=y,this.body=w}return D}();e.LabeledStatement=Cr;var R=function(){function D(y,w){this.type=i.Syntax.Literal,this.value=y,this.raw=w}return D}();e.Literal=R;var z=function(){function D(y,w){this.type=i.Syntax.MetaProperty,this.meta=y,this.property=w}return D}();e.MetaProperty=z;var B=function(){function D(y,w,M,ae,br){this.type=i.Syntax.MethodDefinition,this.key=y,this.computed=w,this.value=M,this.kind=ae,this.static=br}return D}();e.MethodDefinition=B;var H=function(){function D(y){this.type=i.Syntax.Program,this.body=y,this.sourceType="module"}return D}();e.Module=H;var q=function(){function D(y,w){this.type=i.Syntax.NewExpression,this.callee=y,this.arguments=w}return D}();e.NewExpression=q;var Q=function(){function D(y){this.type=i.Syntax.ObjectExpression,this.properties=y}return D}();e.ObjectExpression=Q;var K=function(){function D(y){this.type=i.Syntax.ObjectPattern,this.properties=y}return D}();e.ObjectPattern=K;var pt=function(){function D(y,w,M,ae,br,yl){this.type=i.Syntax.Property,this.key=w,this.computed=M,this.value=ae,this.kind=y,this.method=br,this.shorthand=yl}return D}();e.Property=pt;var et=function(){function D(y,w,M,ae){this.type=i.Syntax.Literal,this.value=y,this.raw=w,this.regex={pattern:M,flags:ae}}return D}();e.RegexLiteral=et;var Qc=function(){function D(y){this.type=i.Syntax.RestElement,this.argument=y}return D}();e.RestElement=Qc;var Zc=function(){function D(y){this.type=i.Syntax.ReturnStatement,this.argument=y}return D}();e.ReturnStatement=Zc;var el=function(){function D(y){this.type=i.Syntax.Program,this.body=y,this.sourceType="script"}return D}();e.Script=el;var tl=function(){function D(y){this.type=i.Syntax.SequenceExpression,this.expressions=y}return D}();e.SequenceExpression=tl;var rl=function(){function D(y){this.type=i.Syntax.SpreadElement,this.argument=y}return D}();e.SpreadElement=rl;var il=function(){function D(y,w){this.type=i.Syntax.MemberExpression,this.computed=!1,this.object=y,this.property=w}return D}();e.StaticMemberExpression=il;var nl=function(){function D(){this.type=i.Syntax.Super}return D}();e.Super=nl;var sl=function(){function D(y,w){this.type=i.Syntax.SwitchCase,this.test=y,this.consequent=w}return D}();e.SwitchCase=sl;var al=function(){function D(y,w){this.type=i.Syntax.SwitchStatement,this.discriminant=y,this.cases=w}return D}();e.SwitchStatement=al;var ol=function(){function D(y,w){this.type=i.Syntax.TaggedTemplateExpression,this.tag=y,this.quasi=w}return D}();e.TaggedTemplateExpression=ol;var ul=function(){function D(y,w){this.type=i.Syntax.TemplateElement,this.value=y,this.tail=w}return D}();e.TemplateElement=ul;var cl=function(){function D(y,w){this.type=i.Syntax.TemplateLiteral,this.quasis=y,this.expressions=w}return D}();e.TemplateLiteral=cl;var ll=function(){function D(){this.type=i.Syntax.ThisExpression}return D}();e.ThisExpression=ll;var hl=function(){function D(y){this.type=i.Syntax.ThrowStatement,this.argument=y}return D}();e.ThrowStatement=hl;var fl=function(){function D(y,w,M){this.type=i.Syntax.TryStatement,this.block=y,this.handler=w,this.finalizer=M}return D}();e.TryStatement=fl;var pl=function(){function D(y,w){this.type=i.Syntax.UnaryExpression,this.operator=y,this.argument=w,this.prefix=!0}return D}();e.UnaryExpression=pl;var dl=function(){function D(y,w,M){this.type=i.Syntax.UpdateExpression,this.operator=y,this.argument=w,this.prefix=M}return D}();e.UpdateExpression=dl;var ml=function(){function D(y,w){this.type=i.Syntax.VariableDeclaration,this.declarations=y,this.kind=w}return D}();e.VariableDeclaration=ml;var xl=function(){function D(y,w){this.type=i.Syntax.VariableDeclarator,this.id=y,this.init=w}return D}();e.VariableDeclarator=xl;var El=function(){function D(y,w){this.type=i.Syntax.WhileStatement,this.test=y,this.body=w}return D}();e.WhileStatement=El;var gl=function(){function D(y,w){this.type=i.Syntax.WithStatement,this.object=y,this.body=w}return D}();e.WithStatement=gl;var Dl=function(){function D(y,w){this.type=i.Syntax.YieldExpression,this.argument=y,this.delegate=w}return D}();e.YieldExpression=Dl},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(9),a=r(10),c=r(11),u=r(7),x=r(12),d=r(2),p=r(13),h="ArrowParameterPlaceHolder",m=function(){function l(n,s,o){s===void 0&&(s={}),this.config={range:typeof s.range=="boolean"&&s.range,loc:typeof s.loc=="boolean"&&s.loc,source:null,tokens:typeof s.tokens=="boolean"&&s.tokens,comment:typeof s.comment=="boolean"&&s.comment,tolerant:typeof s.tolerant=="boolean"&&s.tolerant},this.config.loc&&s.source&&s.source!==null&&(this.config.source=String(s.source)),this.delegate=o,this.errorHandler=new a.ErrorHandler,this.errorHandler.tolerant=this.config.tolerant,this.scanner=new x.Scanner(n,this.errorHandler),this.scanner.trackComment=this.config.comment,this.operatorPrecedence={")":0,";":0,",":0,"=":0,"]":0,"||":1,"&&":2,"|":3,"^":4,"&":5,"==":6,"!=":6,"===":6,"!==":6,"<":7,">":7,"<=":7,">=":7,"<<":8,">>":8,">>>":8,"+":9,"-":9,"*":11,"/":11,"%":11},this.lookahead={type:2,value:"",lineNumber:this.scanner.lineNumber,lineStart:0,start:0,end:0},this.hasLineTerminator=!1,this.context={isModule:!1,await:!1,allowIn:!0,allowStrictDirective:!0,allowYield:!0,firstCoverInitializedNameError:null,isAssignmentTarget:!1,isBindingElement:!1,inFunctionBody:!1,inIteration:!1,inSwitch:!1,labelSet:{},strict:!1},this.tokens=[],this.startMarker={index:0,line:this.scanner.lineNumber,column:0},this.lastMarker={index:0,line:this.scanner.lineNumber,column:0},this.nextToken(),this.lastMarker={index:this.scanner.index,line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart}}return l.prototype.throwError=function(n){for(var s=[],o=1;o0&&this.delegate)for(var s=0;s>="||n===">>>="||n==="&="||n==="^="||n==="|="},l.prototype.isolateCoverGrammar=function(n){var s=this.context.isBindingElement,o=this.context.isAssignmentTarget,f=this.context.firstCoverInitializedNameError;this.context.isBindingElement=!0,this.context.isAssignmentTarget=!0,this.context.firstCoverInitializedNameError=null;var E=n.call(this);return this.context.firstCoverInitializedNameError!==null&&this.throwUnexpectedToken(this.context.firstCoverInitializedNameError),this.context.isBindingElement=s,this.context.isAssignmentTarget=o,this.context.firstCoverInitializedNameError=f,E},l.prototype.inheritCoverGrammar=function(n){var s=this.context.isBindingElement,o=this.context.isAssignmentTarget,f=this.context.firstCoverInitializedNameError;this.context.isBindingElement=!0,this.context.isAssignmentTarget=!0,this.context.firstCoverInitializedNameError=null;var E=n.call(this);return this.context.isBindingElement=this.context.isBindingElement&&s,this.context.isAssignmentTarget=this.context.isAssignmentTarget&&o,this.context.firstCoverInitializedNameError=f||this.context.firstCoverInitializedNameError,E},l.prototype.consumeSemicolon=function(){this.match(";")?this.nextToken():this.hasLineTerminator||(this.lookahead.type!==2&&!this.match("}")&&this.throwUnexpectedToken(this.lookahead),this.lastMarker.index=this.startMarker.index,this.lastMarker.line=this.startMarker.line,this.lastMarker.column=this.startMarker.column)},l.prototype.parsePrimaryExpression=function(){var n=this.createNode(),s,o,f;switch(this.lookahead.type){case 3:(this.context.isModule||this.context.await)&&this.lookahead.value==="await"&&this.tolerateUnexpectedToken(this.lookahead),s=this.matchAsyncFunction()?this.parseFunctionExpression():this.finalize(n,new u.Identifier(this.nextToken().value));break;case 6:case 8:this.context.strict&&this.lookahead.octal&&this.tolerateUnexpectedToken(this.lookahead,c.Messages.StrictOctalLiteral),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,o=this.nextToken(),f=this.getTokenRaw(o),s=this.finalize(n,new u.Literal(o.value,f));break;case 1:this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,o=this.nextToken(),f=this.getTokenRaw(o),s=this.finalize(n,new u.Literal(o.value==="true",f));break;case 5:this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,o=this.nextToken(),f=this.getTokenRaw(o),s=this.finalize(n,new u.Literal(null,f));break;case 10:s=this.parseTemplateLiteral();break;case 7:switch(this.lookahead.value){case"(":this.context.isBindingElement=!1,s=this.inheritCoverGrammar(this.parseGroupExpression);break;case"[":s=this.inheritCoverGrammar(this.parseArrayInitializer);break;case"{":s=this.inheritCoverGrammar(this.parseObjectInitializer);break;case"/":case"/=":this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,this.scanner.index=this.startMarker.index,o=this.nextRegexToken(),f=this.getTokenRaw(o),s=this.finalize(n,new u.RegexLiteral(o.regex,f,o.pattern,o.flags));break;default:s=this.throwUnexpectedToken(this.nextToken())}break;case 4:!this.context.strict&&this.context.allowYield&&this.matchKeyword("yield")?s=this.parseIdentifierName():!this.context.strict&&this.matchKeyword("let")?s=this.finalize(n,new u.Identifier(this.nextToken().value)):(this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,this.matchKeyword("function")?s=this.parseFunctionExpression():this.matchKeyword("this")?(this.nextToken(),s=this.finalize(n,new u.ThisExpression)):this.matchKeyword("class")?s=this.parseClassExpression():s=this.throwUnexpectedToken(this.nextToken()));break;default:s=this.throwUnexpectedToken(this.nextToken())}return s},l.prototype.parseSpreadElement=function(){var n=this.createNode();this.expect("...");var s=this.inheritCoverGrammar(this.parseAssignmentExpression);return this.finalize(n,new u.SpreadElement(s))},l.prototype.parseArrayInitializer=function(){var n=this.createNode(),s=[];for(this.expect("[");!this.match("]");)if(this.match(","))this.nextToken(),s.push(null);else if(this.match("...")){var o=this.parseSpreadElement();this.match("]")||(this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,this.expect(",")),s.push(o)}else s.push(this.inheritCoverGrammar(this.parseAssignmentExpression)),this.match("]")||this.expect(",");return this.expect("]"),this.finalize(n,new u.ArrayExpression(s))},l.prototype.parsePropertyMethod=function(n){this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1;var s=this.context.strict,o=this.context.allowStrictDirective;this.context.allowStrictDirective=n.simple;var f=this.isolateCoverGrammar(this.parseFunctionSourceElements);return this.context.strict&&n.firstRestricted&&this.tolerateUnexpectedToken(n.firstRestricted,n.message),this.context.strict&&n.stricted&&this.tolerateUnexpectedToken(n.stricted,n.message),this.context.strict=s,this.context.allowStrictDirective=o,f},l.prototype.parsePropertyMethodFunction=function(){var n=!1,s=this.createNode(),o=this.context.allowYield;this.context.allowYield=!0;var f=this.parseFormalParameters(),E=this.parsePropertyMethod(f);return this.context.allowYield=o,this.finalize(s,new u.FunctionExpression(null,f.params,E,n))},l.prototype.parsePropertyMethodAsyncFunction=function(){var n=this.createNode(),s=this.context.allowYield,o=this.context.await;this.context.allowYield=!1,this.context.await=!0;var f=this.parseFormalParameters(),E=this.parsePropertyMethod(f);return this.context.allowYield=s,this.context.await=o,this.finalize(n,new u.AsyncFunctionExpression(null,f.params,E))},l.prototype.parseObjectPropertyKey=function(){var n=this.createNode(),s=this.nextToken(),o;switch(s.type){case 8:case 6:this.context.strict&&s.octal&&this.tolerateUnexpectedToken(s,c.Messages.StrictOctalLiteral);var f=this.getTokenRaw(s);o=this.finalize(n,new u.Literal(s.value,f));break;case 3:case 1:case 5:case 4:o=this.finalize(n,new u.Identifier(s.value));break;case 7:s.value==="["?(o=this.isolateCoverGrammar(this.parseAssignmentExpression),this.expect("]")):o=this.throwUnexpectedToken(s);break;default:o=this.throwUnexpectedToken(s)}return o},l.prototype.isPropertyKey=function(n,s){return n.type===d.Syntax.Identifier&&n.name===s||n.type===d.Syntax.Literal&&n.value===s},l.prototype.parseObjectProperty=function(n){var s=this.createNode(),o=this.lookahead,f,E=null,g=null,v=!1,F=!1,A=!1,T=!1;if(o.type===3){var k=o.value;this.nextToken(),v=this.match("["),T=!this.hasLineTerminator&&k==="async"&&!this.match(":")&&!this.match("(")&&!this.match("*")&&!this.match(","),E=T?this.parseObjectPropertyKey():this.finalize(s,new u.Identifier(k))}else this.match("*")?this.nextToken():(v=this.match("["),E=this.parseObjectPropertyKey());var P=this.qualifiedPropertyName(this.lookahead);if(o.type===3&&!T&&o.value==="get"&&P)f="get",v=this.match("["),E=this.parseObjectPropertyKey(),this.context.allowYield=!1,g=this.parseGetterMethod();else if(o.type===3&&!T&&o.value==="set"&&P)f="set",v=this.match("["),E=this.parseObjectPropertyKey(),g=this.parseSetterMethod();else if(o.type===7&&o.value==="*"&&P)f="init",v=this.match("["),E=this.parseObjectPropertyKey(),g=this.parseGeneratorMethod(),F=!0;else if(E||this.throwUnexpectedToken(this.lookahead),f="init",this.match(":")&&!T)!v&&this.isPropertyKey(E,"__proto__")&&(n.value&&this.tolerateError(c.Messages.DuplicateProtoProperty),n.value=!0),this.nextToken(),g=this.inheritCoverGrammar(this.parseAssignmentExpression);else if(this.match("("))g=T?this.parsePropertyMethodAsyncFunction():this.parsePropertyMethodFunction(),F=!0;else if(o.type===3){var k=this.finalize(s,new u.Identifier(o.value));if(this.match("=")){this.context.firstCoverInitializedNameError=this.lookahead,this.nextToken(),A=!0;var N=this.isolateCoverGrammar(this.parseAssignmentExpression);g=this.finalize(s,new u.AssignmentPattern(k,N))}else A=!0,g=k}else this.throwUnexpectedToken(this.nextToken());return this.finalize(s,new u.Property(f,E,v,g,F,A))},l.prototype.parseObjectInitializer=function(){var n=this.createNode();this.expect("{");for(var s=[],o={value:!1};!this.match("}");)s.push(this.parseObjectProperty(o)),this.match("}")||this.expectCommaSeparator();return this.expect("}"),this.finalize(n,new u.ObjectExpression(s))},l.prototype.parseTemplateHead=function(){i.assert(this.lookahead.head,"Template literal must start with a template head");var n=this.createNode(),s=this.nextToken(),o=s.value,f=s.cooked;return this.finalize(n,new u.TemplateElement({raw:o,cooked:f},s.tail))},l.prototype.parseTemplateElement=function(){this.lookahead.type!==10&&this.throwUnexpectedToken();var n=this.createNode(),s=this.nextToken(),o=s.value,f=s.cooked;return this.finalize(n,new u.TemplateElement({raw:o,cooked:f},s.tail))},l.prototype.parseTemplateLiteral=function(){var n=this.createNode(),s=[],o=[],f=this.parseTemplateHead();for(o.push(f);!f.tail;)s.push(this.parseExpression()),f=this.parseTemplateElement(),o.push(f);return this.finalize(n,new u.TemplateLiteral(o,s))},l.prototype.reinterpretExpressionAsPattern=function(n){switch(n.type){case d.Syntax.Identifier:case d.Syntax.MemberExpression:case d.Syntax.RestElement:case d.Syntax.AssignmentPattern:break;case d.Syntax.SpreadElement:n.type=d.Syntax.RestElement,this.reinterpretExpressionAsPattern(n.argument);break;case d.Syntax.ArrayExpression:n.type=d.Syntax.ArrayPattern;for(var s=0;s")||this.expect("=>"),n={type:h,params:[],async:!1};else{var s=this.lookahead,o=[];if(this.match("..."))n=this.parseRestElement(o),this.expect(")"),this.match("=>")||this.expect("=>"),n={type:h,params:[n],async:!1};else{var f=!1;if(this.context.isBindingElement=!0,n=this.inheritCoverGrammar(this.parseAssignmentExpression),this.match(",")){var E=[];for(this.context.isAssignmentTarget=!1,E.push(n);this.lookahead.type!==2&&this.match(",");){if(this.nextToken(),this.match(")")){this.nextToken();for(var g=0;g")||this.expect("=>"),this.context.isBindingElement=!1;for(var g=0;g")&&(n.type===d.Syntax.Identifier&&n.name==="yield"&&(f=!0,n={type:h,params:[n],async:!1}),!f)){if(this.context.isBindingElement||this.throwUnexpectedToken(this.lookahead),n.type===d.Syntax.SequenceExpression)for(var g=0;g")){for(var F=0;F0){this.nextToken(),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1;for(var E=[n,this.lookahead],g=s,v=this.isolateCoverGrammar(this.parseExponentiationExpression),F=[g,o.value,v],A=[f];f=this.binaryPrecedence(this.lookahead),!(f<=0);){for(;F.length>2&&f<=A[A.length-1];){v=F.pop();var T=F.pop();A.pop(),g=F.pop(),E.pop();var k=this.startNode(E[E.length-1]);F.push(this.finalize(k,new u.BinaryExpression(T,g,v)))}F.push(this.nextToken().value),A.push(f),E.push(this.lookahead),F.push(this.isolateCoverGrammar(this.parseExponentiationExpression))}var P=F.length-1;s=F[P];for(var N=E.pop();P>1;){var U=E.pop(),S=N&&N.lineStart,k=this.startNode(U,S),T=F[P-1];s=this.finalize(k,new u.BinaryExpression(T,F[P-2],s)),P-=2,N=U}}return s},l.prototype.parseConditionalExpression=function(){var n=this.lookahead,s=this.inheritCoverGrammar(this.parseBinaryExpression);if(this.match("?")){this.nextToken();var o=this.context.allowIn;this.context.allowIn=!0;var f=this.isolateCoverGrammar(this.parseAssignmentExpression);this.context.allowIn=o,this.expect(":");var E=this.isolateCoverGrammar(this.parseAssignmentExpression);s=this.finalize(this.startNode(n),new u.ConditionalExpression(s,f,E)),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1}return s},l.prototype.checkPatternParam=function(n,s){switch(s.type){case d.Syntax.Identifier:this.validateParam(n,s,s.name);break;case d.Syntax.RestElement:this.checkPatternParam(n,s.argument);break;case d.Syntax.AssignmentPattern:this.checkPatternParam(n,s.left);break;case d.Syntax.ArrayPattern:for(var o=0;o")){this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1;var E=n.async,g=this.reinterpretAsCoverFormalsList(n);if(g){this.hasLineTerminator&&this.tolerateUnexpectedToken(this.lookahead),this.context.firstCoverInitializedNameError=null;var v=this.context.strict,F=this.context.allowStrictDirective;this.context.allowStrictDirective=g.simple;var A=this.context.allowYield,T=this.context.await;this.context.allowYield=!0,this.context.await=E;var k=this.startNode(s);this.expect("=>");var P=void 0;if(this.match("{")){var N=this.context.allowIn;this.context.allowIn=!0,P=this.parseFunctionSourceElements(),this.context.allowIn=N}else P=this.isolateCoverGrammar(this.parseAssignmentExpression);var U=P.type!==d.Syntax.BlockStatement;this.context.strict&&g.firstRestricted&&this.throwUnexpectedToken(g.firstRestricted,g.message),this.context.strict&&g.stricted&&this.tolerateUnexpectedToken(g.stricted,g.message),n=E?this.finalize(k,new u.AsyncArrowFunctionExpression(g.params,P,U)):this.finalize(k,new u.ArrowFunctionExpression(g.params,P,U)),this.context.strict=v,this.context.allowStrictDirective=F,this.context.allowYield=A,this.context.await=T}}else if(this.matchAssign()){if(this.context.isAssignmentTarget||this.tolerateError(c.Messages.InvalidLHSInAssignment),this.context.strict&&n.type===d.Syntax.Identifier){var S=n;this.scanner.isRestrictedWord(S.name)&&this.tolerateUnexpectedToken(o,c.Messages.StrictLHSAssignment),this.scanner.isStrictModeReservedWord(S.name)&&this.tolerateUnexpectedToken(o,c.Messages.StrictReservedWord)}this.match("=")?this.reinterpretExpressionAsPattern(n):(this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1),o=this.nextToken();var j=o.value,J=this.isolateCoverGrammar(this.parseAssignmentExpression);n=this.finalize(this.startNode(s),new u.AssignmentExpression(j,n,J)),this.context.firstCoverInitializedNameError=null}}return n},l.prototype.parseExpression=function(){var n=this.lookahead,s=this.isolateCoverGrammar(this.parseAssignmentExpression);if(this.match(",")){var o=[];for(o.push(s);this.lookahead.type!==2&&this.match(",");)this.nextToken(),o.push(this.isolateCoverGrammar(this.parseAssignmentExpression));s=this.finalize(this.startNode(n),new u.SequenceExpression(o))}return s},l.prototype.parseStatementListItem=function(){var n;if(this.context.isAssignmentTarget=!0,this.context.isBindingElement=!0,this.lookahead.type===4)switch(this.lookahead.value){case"export":this.context.isModule||this.tolerateUnexpectedToken(this.lookahead,c.Messages.IllegalExportDeclaration),n=this.parseExportDeclaration();break;case"import":this.context.isModule||this.tolerateUnexpectedToken(this.lookahead,c.Messages.IllegalImportDeclaration),n=this.parseImportDeclaration();break;case"const":n=this.parseLexicalDeclaration({inFor:!1});break;case"function":n=this.parseFunctionDeclaration();break;case"class":n=this.parseClassDeclaration();break;case"let":n=this.isLexicalDeclaration()?this.parseLexicalDeclaration({inFor:!1}):this.parseStatement();break;default:n=this.parseStatement();break}else n=this.parseStatement();return n},l.prototype.parseBlock=function(){var n=this.createNode();this.expect("{");for(var s=[];!this.match("}");)s.push(this.parseStatementListItem());return this.expect("}"),this.finalize(n,new u.BlockStatement(s))},l.prototype.parseLexicalBinding=function(n,s){var o=this.createNode(),f=[],E=this.parsePattern(f,n);this.context.strict&&E.type===d.Syntax.Identifier&&this.scanner.isRestrictedWord(E.name)&&this.tolerateError(c.Messages.StrictVarName);var g=null;return n==="const"?!this.matchKeyword("in")&&!this.matchContextualKeyword("of")&&(this.match("=")?(this.nextToken(),g=this.isolateCoverGrammar(this.parseAssignmentExpression)):this.throwError(c.Messages.DeclarationMissingInitializer,"const")):(!s.inFor&&E.type!==d.Syntax.Identifier||this.match("="))&&(this.expect("="),g=this.isolateCoverGrammar(this.parseAssignmentExpression)),this.finalize(o,new u.VariableDeclarator(E,g))},l.prototype.parseBindingList=function(n,s){for(var o=[this.parseLexicalBinding(n,s)];this.match(",");)this.nextToken(),o.push(this.parseLexicalBinding(n,s));return o},l.prototype.isLexicalDeclaration=function(){var n=this.scanner.saveState();this.scanner.scanComments();var s=this.scanner.lex();return this.scanner.restoreState(n),s.type===3||s.type===7&&s.value==="["||s.type===7&&s.value==="{"||s.type===4&&s.value==="let"||s.type===4&&s.value==="yield"},l.prototype.parseLexicalDeclaration=function(n){var s=this.createNode(),o=this.nextToken().value;i.assert(o==="let"||o==="const","Lexical declaration must be either let or const");var f=this.parseBindingList(o,n);return this.consumeSemicolon(),this.finalize(s,new u.VariableDeclaration(f,o))},l.prototype.parseBindingRestElement=function(n,s){var o=this.createNode();this.expect("...");var f=this.parsePattern(n,s);return this.finalize(o,new u.RestElement(f))},l.prototype.parseArrayPattern=function(n,s){var o=this.createNode();this.expect("[");for(var f=[];!this.match("]");)if(this.match(","))this.nextToken(),f.push(null);else{if(this.match("...")){f.push(this.parseBindingRestElement(n,s));break}else f.push(this.parsePatternWithDefault(n,s));this.match("]")||this.expect(",")}return this.expect("]"),this.finalize(o,new u.ArrayPattern(f))},l.prototype.parsePropertyPattern=function(n,s){var o=this.createNode(),f=!1,E=!1,g=!1,v,F;if(this.lookahead.type===3){var A=this.lookahead;v=this.parseVariableIdentifier();var T=this.finalize(o,new u.Identifier(A.value));if(this.match("=")){n.push(A),E=!0,this.nextToken();var k=this.parseAssignmentExpression();F=this.finalize(this.startNode(A),new u.AssignmentPattern(T,k))}else this.match(":")?(this.expect(":"),F=this.parsePatternWithDefault(n,s)):(n.push(A),E=!0,F=T)}else f=this.match("["),v=this.parseObjectPropertyKey(),this.expect(":"),F=this.parsePatternWithDefault(n,s);return this.finalize(o,new u.Property("init",v,f,F,g,E))},l.prototype.parseObjectPattern=function(n,s){var o=this.createNode(),f=[];for(this.expect("{");!this.match("}");)f.push(this.parsePropertyPattern(n,s)),this.match("}")||this.expect(",");return this.expect("}"),this.finalize(o,new u.ObjectPattern(f))},l.prototype.parsePattern=function(n,s){var o;return this.match("[")?o=this.parseArrayPattern(n,s):this.match("{")?o=this.parseObjectPattern(n,s):(this.matchKeyword("let")&&(s==="const"||s==="let")&&this.tolerateUnexpectedToken(this.lookahead,c.Messages.LetInLexicalBinding),n.push(this.lookahead),o=this.parseVariableIdentifier(s)),o},l.prototype.parsePatternWithDefault=function(n,s){var o=this.lookahead,f=this.parsePattern(n,s);if(this.match("=")){this.nextToken();var E=this.context.allowYield;this.context.allowYield=!0;var g=this.isolateCoverGrammar(this.parseAssignmentExpression);this.context.allowYield=E,f=this.finalize(this.startNode(o),new u.AssignmentPattern(f,g))}return f},l.prototype.parseVariableIdentifier=function(n){var s=this.createNode(),o=this.nextToken();return o.type===4&&o.value==="yield"?this.context.strict?this.tolerateUnexpectedToken(o,c.Messages.StrictReservedWord):this.context.allowYield||this.throwUnexpectedToken(o):o.type!==3?this.context.strict&&o.type===4&&this.scanner.isStrictModeReservedWord(o.value)?this.tolerateUnexpectedToken(o,c.Messages.StrictReservedWord):(this.context.strict||o.value!=="let"||n!=="var")&&this.throwUnexpectedToken(o):(this.context.isModule||this.context.await)&&o.type===3&&o.value==="await"&&this.tolerateUnexpectedToken(o),this.finalize(s,new u.Identifier(o.value))},l.prototype.parseVariableDeclaration=function(n){var s=this.createNode(),o=[],f=this.parsePattern(o,"var");this.context.strict&&f.type===d.Syntax.Identifier&&this.scanner.isRestrictedWord(f.name)&&this.tolerateError(c.Messages.StrictVarName);var E=null;return this.match("=")?(this.nextToken(),E=this.isolateCoverGrammar(this.parseAssignmentExpression)):f.type!==d.Syntax.Identifier&&!n.inFor&&this.expect("="),this.finalize(s,new u.VariableDeclarator(f,E))},l.prototype.parseVariableDeclarationList=function(n){var s={inFor:n.inFor},o=[];for(o.push(this.parseVariableDeclaration(s));this.match(",");)this.nextToken(),o.push(this.parseVariableDeclaration(s));return o},l.prototype.parseVariableStatement=function(){var n=this.createNode();this.expectKeyword("var");var s=this.parseVariableDeclarationList({inFor:!1});return this.consumeSemicolon(),this.finalize(n,new u.VariableDeclaration(s,"var"))},l.prototype.parseEmptyStatement=function(){var n=this.createNode();return this.expect(";"),this.finalize(n,new u.EmptyStatement)},l.prototype.parseExpressionStatement=function(){var n=this.createNode(),s=this.parseExpression();return this.consumeSemicolon(),this.finalize(n,new u.ExpressionStatement(s))},l.prototype.parseIfClause=function(){return this.context.strict&&this.matchKeyword("function")&&this.tolerateError(c.Messages.StrictFunction),this.parseStatement()},l.prototype.parseIfStatement=function(){var n=this.createNode(),s,o=null;this.expectKeyword("if"),this.expect("(");var f=this.parseExpression();return!this.match(")")&&this.config.tolerant?(this.tolerateUnexpectedToken(this.nextToken()),s=this.finalize(this.createNode(),new u.EmptyStatement)):(this.expect(")"),s=this.parseIfClause(),this.matchKeyword("else")&&(this.nextToken(),o=this.parseIfClause())),this.finalize(n,new u.IfStatement(f,s,o))},l.prototype.parseDoWhileStatement=function(){var n=this.createNode();this.expectKeyword("do");var s=this.context.inIteration;this.context.inIteration=!0;var o=this.parseStatement();this.context.inIteration=s,this.expectKeyword("while"),this.expect("(");var f=this.parseExpression();return!this.match(")")&&this.config.tolerant?this.tolerateUnexpectedToken(this.nextToken()):(this.expect(")"),this.match(";")&&this.nextToken()),this.finalize(n,new u.DoWhileStatement(o,f))},l.prototype.parseWhileStatement=function(){var n=this.createNode(),s;this.expectKeyword("while"),this.expect("(");var o=this.parseExpression();if(!this.match(")")&&this.config.tolerant)this.tolerateUnexpectedToken(this.nextToken()),s=this.finalize(this.createNode(),new u.EmptyStatement);else{this.expect(")");var f=this.context.inIteration;this.context.inIteration=!0,s=this.parseStatement(),this.context.inIteration=f}return this.finalize(n,new u.WhileStatement(o,s))},l.prototype.parseForStatement=function(){var n=null,s=null,o=null,f=!0,E,g,v=this.createNode();if(this.expectKeyword("for"),this.expect("("),this.match(";"))this.nextToken();else if(this.matchKeyword("var")){n=this.createNode(),this.nextToken();var F=this.context.allowIn;this.context.allowIn=!1;var A=this.parseVariableDeclarationList({inFor:!0});if(this.context.allowIn=F,A.length===1&&this.matchKeyword("in")){var T=A[0];T.init&&(T.id.type===d.Syntax.ArrayPattern||T.id.type===d.Syntax.ObjectPattern||this.context.strict)&&this.tolerateError(c.Messages.ForInOfLoopInitializer,"for-in"),n=this.finalize(n,new u.VariableDeclaration(A,"var")),this.nextToken(),E=n,g=this.parseExpression(),n=null}else A.length===1&&A[0].init===null&&this.matchContextualKeyword("of")?(n=this.finalize(n,new u.VariableDeclaration(A,"var")),this.nextToken(),E=n,g=this.parseAssignmentExpression(),n=null,f=!1):(n=this.finalize(n,new u.VariableDeclaration(A,"var")),this.expect(";"))}else if(this.matchKeyword("const")||this.matchKeyword("let")){n=this.createNode();var k=this.nextToken().value;if(!this.context.strict&&this.lookahead.value==="in")n=this.finalize(n,new u.Identifier(k)),this.nextToken(),E=n,g=this.parseExpression(),n=null;else{var F=this.context.allowIn;this.context.allowIn=!1;var A=this.parseBindingList(k,{inFor:!0});this.context.allowIn=F,A.length===1&&A[0].init===null&&this.matchKeyword("in")?(n=this.finalize(n,new u.VariableDeclaration(A,k)),this.nextToken(),E=n,g=this.parseExpression(),n=null):A.length===1&&A[0].init===null&&this.matchContextualKeyword("of")?(n=this.finalize(n,new u.VariableDeclaration(A,k)),this.nextToken(),E=n,g=this.parseAssignmentExpression(),n=null,f=!1):(this.consumeSemicolon(),n=this.finalize(n,new u.VariableDeclaration(A,k)))}}else{var P=this.lookahead,F=this.context.allowIn;if(this.context.allowIn=!1,n=this.inheritCoverGrammar(this.parseAssignmentExpression),this.context.allowIn=F,this.matchKeyword("in"))(!this.context.isAssignmentTarget||n.type===d.Syntax.AssignmentExpression)&&this.tolerateError(c.Messages.InvalidLHSInForIn),this.nextToken(),this.reinterpretExpressionAsPattern(n),E=n,g=this.parseExpression(),n=null;else if(this.matchContextualKeyword("of"))(!this.context.isAssignmentTarget||n.type===d.Syntax.AssignmentExpression)&&this.tolerateError(c.Messages.InvalidLHSInForLoop),this.nextToken(),this.reinterpretExpressionAsPattern(n),E=n,g=this.parseAssignmentExpression(),n=null,f=!1;else{if(this.match(",")){for(var N=[n];this.match(",");)this.nextToken(),N.push(this.isolateCoverGrammar(this.parseAssignmentExpression));n=this.finalize(this.startNode(P),new u.SequenceExpression(N))}this.expect(";")}}typeof E>"u"&&(this.match(";")||(s=this.parseExpression()),this.expect(";"),this.match(")")||(o=this.parseExpression()));var U;if(!this.match(")")&&this.config.tolerant)this.tolerateUnexpectedToken(this.nextToken()),U=this.finalize(this.createNode(),new u.EmptyStatement);else{this.expect(")");var S=this.context.inIteration;this.context.inIteration=!0,U=this.isolateCoverGrammar(this.parseStatement),this.context.inIteration=S}return typeof E>"u"?this.finalize(v,new u.ForStatement(n,s,o,U)):f?this.finalize(v,new u.ForInStatement(E,g,U)):this.finalize(v,new u.ForOfStatement(E,g,U))},l.prototype.parseContinueStatement=function(){var n=this.createNode();this.expectKeyword("continue");var s=null;if(this.lookahead.type===3&&!this.hasLineTerminator){var o=this.parseVariableIdentifier();s=o;var f="$"+o.name;Object.prototype.hasOwnProperty.call(this.context.labelSet,f)||this.throwError(c.Messages.UnknownLabel,o.name)}return this.consumeSemicolon(),s===null&&!this.context.inIteration&&this.throwError(c.Messages.IllegalContinue),this.finalize(n,new u.ContinueStatement(s))},l.prototype.parseBreakStatement=function(){var n=this.createNode();this.expectKeyword("break");var s=null;if(this.lookahead.type===3&&!this.hasLineTerminator){var o=this.parseVariableIdentifier(),f="$"+o.name;Object.prototype.hasOwnProperty.call(this.context.labelSet,f)||this.throwError(c.Messages.UnknownLabel,o.name),s=o}return this.consumeSemicolon(),s===null&&!this.context.inIteration&&!this.context.inSwitch&&this.throwError(c.Messages.IllegalBreak),this.finalize(n,new u.BreakStatement(s))},l.prototype.parseReturnStatement=function(){this.context.inFunctionBody||this.tolerateError(c.Messages.IllegalReturn);var n=this.createNode();this.expectKeyword("return");var s=!this.match(";")&&!this.match("}")&&!this.hasLineTerminator&&this.lookahead.type!==2||this.lookahead.type===8||this.lookahead.type===10,o=s?this.parseExpression():null;return this.consumeSemicolon(),this.finalize(n,new u.ReturnStatement(o))},l.prototype.parseWithStatement=function(){this.context.strict&&this.tolerateError(c.Messages.StrictModeWith);var n=this.createNode(),s;this.expectKeyword("with"),this.expect("(");var o=this.parseExpression();return!this.match(")")&&this.config.tolerant?(this.tolerateUnexpectedToken(this.nextToken()),s=this.finalize(this.createNode(),new u.EmptyStatement)):(this.expect(")"),s=this.parseStatement()),this.finalize(n,new u.WithStatement(o,s))},l.prototype.parseSwitchCase=function(){var n=this.createNode(),s;this.matchKeyword("default")?(this.nextToken(),s=null):(this.expectKeyword("case"),s=this.parseExpression()),this.expect(":");for(var o=[];!(this.match("}")||this.matchKeyword("default")||this.matchKeyword("case"));)o.push(this.parseStatementListItem());return this.finalize(n,new u.SwitchCase(s,o))},l.prototype.parseSwitchStatement=function(){var n=this.createNode();this.expectKeyword("switch"),this.expect("(");var s=this.parseExpression();this.expect(")");var o=this.context.inSwitch;this.context.inSwitch=!0;var f=[],E=!1;for(this.expect("{");!this.match("}");){var g=this.parseSwitchCase();g.test===null&&(E&&this.throwError(c.Messages.MultipleDefaultsInSwitch),E=!0),f.push(g)}return this.expect("}"),this.context.inSwitch=o,this.finalize(n,new u.SwitchStatement(s,f))},l.prototype.parseLabelledStatement=function(){var n=this.createNode(),s=this.parseExpression(),o;if(s.type===d.Syntax.Identifier&&this.match(":")){this.nextToken();var f=s,E="$"+f.name;Object.prototype.hasOwnProperty.call(this.context.labelSet,E)&&this.throwError(c.Messages.Redeclaration,"Label",f.name),this.context.labelSet[E]=!0;var g=void 0;if(this.matchKeyword("class"))this.tolerateUnexpectedToken(this.lookahead),g=this.parseClassDeclaration();else if(this.matchKeyword("function")){var v=this.lookahead,F=this.parseFunctionDeclaration();this.context.strict?this.tolerateUnexpectedToken(v,c.Messages.StrictFunction):F.generator&&this.tolerateUnexpectedToken(v,c.Messages.GeneratorInLegacyContext),g=F}else g=this.parseStatement();delete this.context.labelSet[E],o=new u.LabeledStatement(f,g)}else this.consumeSemicolon(),o=new u.ExpressionStatement(s);return this.finalize(n,o)},l.prototype.parseThrowStatement=function(){var n=this.createNode();this.expectKeyword("throw"),this.hasLineTerminator&&this.throwError(c.Messages.NewlineAfterThrow);var s=this.parseExpression();return this.consumeSemicolon(),this.finalize(n,new u.ThrowStatement(s))},l.prototype.parseCatchClause=function(){var n=this.createNode();this.expectKeyword("catch"),this.expect("("),this.match(")")&&this.throwUnexpectedToken(this.lookahead);for(var s=[],o=this.parsePattern(s),f={},E=0;E0&&this.tolerateError(c.Messages.BadGetterArity);var E=this.parsePropertyMethod(f);return this.context.allowYield=o,this.finalize(n,new u.FunctionExpression(null,f.params,E,s))},l.prototype.parseSetterMethod=function(){var n=this.createNode(),s=!1,o=this.context.allowYield;this.context.allowYield=!s;var f=this.parseFormalParameters();f.params.length!==1?this.tolerateError(c.Messages.BadSetterArity):f.params[0]instanceof u.RestElement&&this.tolerateError(c.Messages.BadSetterRestParameter);var E=this.parsePropertyMethod(f);return this.context.allowYield=o,this.finalize(n,new u.FunctionExpression(null,f.params,E,s))},l.prototype.parseGeneratorMethod=function(){var n=this.createNode(),s=!0,o=this.context.allowYield;this.context.allowYield=!0;var f=this.parseFormalParameters();this.context.allowYield=!1;var E=this.parsePropertyMethod(f);return this.context.allowYield=o,this.finalize(n,new u.FunctionExpression(null,f.params,E,s))},l.prototype.isStartOfExpression=function(){var n=!0,s=this.lookahead.value;switch(this.lookahead.type){case 7:n=s==="["||s==="("||s==="{"||s==="+"||s==="-"||s==="!"||s==="~"||s==="++"||s==="--"||s==="/"||s==="/=";break;case 4:n=s==="class"||s==="delete"||s==="function"||s==="let"||s==="new"||s==="super"||s==="this"||s==="typeof"||s==="void"||s==="yield";break;default:break}return n},l.prototype.parseYieldExpression=function(){var n=this.createNode();this.expectKeyword("yield");var s=null,o=!1;if(!this.hasLineTerminator){var f=this.context.allowYield;this.context.allowYield=!1,o=this.match("*"),o?(this.nextToken(),s=this.parseAssignmentExpression()):this.isStartOfExpression()&&(s=this.parseAssignmentExpression()),this.context.allowYield=f}return this.finalize(n,new u.YieldExpression(s,o))},l.prototype.parseClassElement=function(n){var s=this.lookahead,o=this.createNode(),f="",E=null,g=null,v=!1,F=!1,A=!1,T=!1;if(this.match("*"))this.nextToken();else{v=this.match("["),E=this.parseObjectPropertyKey();var k=E;if(k.name==="static"&&(this.qualifiedPropertyName(this.lookahead)||this.match("*"))&&(s=this.lookahead,A=!0,v=this.match("["),this.match("*")?this.nextToken():E=this.parseObjectPropertyKey()),s.type===3&&!this.hasLineTerminator&&s.value==="async"){var P=this.lookahead.value;P!==":"&&P!=="("&&P!=="*"&&(T=!0,s=this.lookahead,E=this.parseObjectPropertyKey(),s.type===3&&s.value==="constructor"&&this.tolerateUnexpectedToken(s,c.Messages.ConstructorIsAsync))}}var N=this.qualifiedPropertyName(this.lookahead);return s.type===3?s.value==="get"&&N?(f="get",v=this.match("["),E=this.parseObjectPropertyKey(),this.context.allowYield=!1,g=this.parseGetterMethod()):s.value==="set"&&N&&(f="set",v=this.match("["),E=this.parseObjectPropertyKey(),g=this.parseSetterMethod()):s.type===7&&s.value==="*"&&N&&(f="init",v=this.match("["),E=this.parseObjectPropertyKey(),g=this.parseGeneratorMethod(),F=!0),!f&&E&&this.match("(")&&(f="init",g=T?this.parsePropertyMethodAsyncFunction():this.parsePropertyMethodFunction(),F=!0),f||this.throwUnexpectedToken(this.lookahead),f==="init"&&(f="method"),v||(A&&this.isPropertyKey(E,"prototype")&&this.throwUnexpectedToken(s,c.Messages.StaticPrototype),!A&&this.isPropertyKey(E,"constructor")&&((f!=="method"||!F||g&&g.generator)&&this.throwUnexpectedToken(s,c.Messages.ConstructorSpecialMethod),n.value?this.throwUnexpectedToken(s,c.Messages.DuplicateConstructor):n.value=!0,f="constructor")),this.finalize(o,new u.MethodDefinition(E,v,g,f,A))},l.prototype.parseClassElementList=function(){var n=[],s={value:!1};for(this.expect("{");!this.match("}");)this.match(";")?this.nextToken():n.push(this.parseClassElement(s));return this.expect("}"),n},l.prototype.parseClassBody=function(){var n=this.createNode(),s=this.parseClassElementList();return this.finalize(n,new u.ClassBody(s))},l.prototype.parseClassDeclaration=function(n){var s=this.createNode(),o=this.context.strict;this.context.strict=!0,this.expectKeyword("class");var f=n&&this.lookahead.type!==3?null:this.parseVariableIdentifier(),E=null;this.matchKeyword("extends")&&(this.nextToken(),E=this.isolateCoverGrammar(this.parseLeftHandSideExpressionAllowCall));var g=this.parseClassBody();return this.context.strict=o,this.finalize(s,new u.ClassDeclaration(f,E,g))},l.prototype.parseClassExpression=function(){var n=this.createNode(),s=this.context.strict;this.context.strict=!0,this.expectKeyword("class");var o=this.lookahead.type===3?this.parseVariableIdentifier():null,f=null;this.matchKeyword("extends")&&(this.nextToken(),f=this.isolateCoverGrammar(this.parseLeftHandSideExpressionAllowCall));var E=this.parseClassBody();return this.context.strict=s,this.finalize(n,new u.ClassExpression(o,f,E))},l.prototype.parseModule=function(){this.context.strict=!0,this.context.isModule=!0,this.scanner.isModule=!0;for(var n=this.createNode(),s=this.parseDirectivePrologues();this.lookahead.type!==2;)s.push(this.parseStatementListItem());return this.finalize(n,new u.Module(s))},l.prototype.parseScript=function(){for(var n=this.createNode(),s=this.parseDirectivePrologues();this.lookahead.type!==2;)s.push(this.parseStatementListItem());return this.finalize(n,new u.Script(s))},l.prototype.parseModuleSpecifier=function(){var n=this.createNode();this.lookahead.type!==8&&this.throwError(c.Messages.InvalidModuleSpecifier);var s=this.nextToken(),o=this.getTokenRaw(s);return this.finalize(n,new u.Literal(s.value,o))},l.prototype.parseImportSpecifier=function(){var n=this.createNode(),s,o;return this.lookahead.type===3?(s=this.parseVariableIdentifier(),o=s,this.matchContextualKeyword("as")&&(this.nextToken(),o=this.parseVariableIdentifier())):(s=this.parseIdentifierName(),o=s,this.matchContextualKeyword("as")?(this.nextToken(),o=this.parseVariableIdentifier()):this.throwUnexpectedToken(this.nextToken())),this.finalize(n,new u.ImportSpecifier(o,s))},l.prototype.parseNamedImports=function(){this.expect("{");for(var n=[];!this.match("}");)n.push(this.parseImportSpecifier()),this.match("}")||this.expect(",");return this.expect("}"),n},l.prototype.parseImportDefaultSpecifier=function(){var n=this.createNode(),s=this.parseIdentifierName();return this.finalize(n,new u.ImportDefaultSpecifier(s))},l.prototype.parseImportNamespaceSpecifier=function(){var n=this.createNode();this.expect("*"),this.matchContextualKeyword("as")||this.throwError(c.Messages.NoAsAfterImportNamespace),this.nextToken();var s=this.parseIdentifierName();return this.finalize(n,new u.ImportNamespaceSpecifier(s))},l.prototype.parseImportDeclaration=function(){this.context.inFunctionBody&&this.throwError(c.Messages.IllegalImportDeclaration);var n=this.createNode();this.expectKeyword("import");var s,o=[];if(this.lookahead.type===8)s=this.parseModuleSpecifier();else{if(this.match("{")?o=o.concat(this.parseNamedImports()):this.match("*")?o.push(this.parseImportNamespaceSpecifier()):this.isIdentifierName(this.lookahead)&&!this.matchKeyword("default")?(o.push(this.parseImportDefaultSpecifier()),this.match(",")&&(this.nextToken(),this.match("*")?o.push(this.parseImportNamespaceSpecifier()):this.match("{")?o=o.concat(this.parseNamedImports()):this.throwUnexpectedToken(this.lookahead))):this.throwUnexpectedToken(this.nextToken()),!this.matchContextualKeyword("from")){var f=this.lookahead.value?c.Messages.UnexpectedToken:c.Messages.MissingFromClause;this.throwError(f,this.lookahead.value)}this.nextToken(),s=this.parseModuleSpecifier()}return this.consumeSemicolon(),this.finalize(n,new u.ImportDeclaration(o,s))},l.prototype.parseExportSpecifier=function(){var n=this.createNode(),s=this.parseIdentifierName(),o=s;return this.matchContextualKeyword("as")&&(this.nextToken(),o=this.parseIdentifierName()),this.finalize(n,new u.ExportSpecifier(s,o))},l.prototype.parseExportDeclaration=function(){this.context.inFunctionBody&&this.throwError(c.Messages.IllegalExportDeclaration);var n=this.createNode();this.expectKeyword("export");var s;if(this.matchKeyword("default"))if(this.nextToken(),this.matchKeyword("function")){var o=this.parseFunctionDeclaration(!0);s=this.finalize(n,new u.ExportDefaultDeclaration(o))}else if(this.matchKeyword("class")){var o=this.parseClassDeclaration(!0);s=this.finalize(n,new u.ExportDefaultDeclaration(o))}else if(this.matchContextualKeyword("async")){var o=this.matchAsyncFunction()?this.parseFunctionDeclaration(!0):this.parseAssignmentExpression();s=this.finalize(n,new u.ExportDefaultDeclaration(o))}else{this.matchContextualKeyword("from")&&this.throwError(c.Messages.UnexpectedToken,this.lookahead.value);var o=this.match("{")?this.parseObjectInitializer():this.match("[")?this.parseArrayInitializer():this.parseAssignmentExpression();this.consumeSemicolon(),s=this.finalize(n,new u.ExportDefaultDeclaration(o))}else if(this.match("*")){if(this.nextToken(),!this.matchContextualKeyword("from")){var f=this.lookahead.value?c.Messages.UnexpectedToken:c.Messages.MissingFromClause;this.throwError(f,this.lookahead.value)}this.nextToken();var E=this.parseModuleSpecifier();this.consumeSemicolon(),s=this.finalize(n,new u.ExportAllDeclaration(E))}else if(this.lookahead.type===4){var o=void 0;switch(this.lookahead.value){case"let":case"const":o=this.parseLexicalDeclaration({inFor:!1});break;case"var":case"class":case"function":o=this.parseStatementListItem();break;default:this.throwUnexpectedToken(this.lookahead)}s=this.finalize(n,new u.ExportNamedDeclaration(o,[],null))}else if(this.matchAsyncFunction()){var o=this.parseFunctionDeclaration();s=this.finalize(n,new u.ExportNamedDeclaration(o,[],null))}else{var g=[],v=null,F=!1;for(this.expect("{");!this.match("}");)F=F||this.matchKeyword("default"),g.push(this.parseExportSpecifier()),this.match("}")||this.expect(",");if(this.expect("}"),this.matchContextualKeyword("from"))this.nextToken(),v=this.parseModuleSpecifier(),this.consumeSemicolon();else if(F){var f=this.lookahead.value?c.Messages.UnexpectedToken:c.Messages.MissingFromClause;this.throwError(f,this.lookahead.value)}else this.consumeSemicolon();s=this.finalize(n,new u.ExportNamedDeclaration(null,g,v))}return s},l}();e.Parser=m},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});function r(i,a){if(!i)throw new Error("ASSERT: "+a)}e.assert=r},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function i(){this.errors=[],this.tolerant=!1}return i.prototype.recordError=function(a){this.errors.push(a)},i.prototype.tolerate=function(a){if(this.tolerant)this.recordError(a);else throw a},i.prototype.constructError=function(a,c){var u=new Error(a);try{throw u}catch(x){Object.create&&Object.defineProperty&&(u=Object.create(x),Object.defineProperty(u,"column",{value:c}))}return u},i.prototype.createError=function(a,c,u,x){var d="Line "+c+": "+x,p=this.constructError(d,u);return p.index=a,p.lineNumber=c,p.description=x,p},i.prototype.throwError=function(a,c,u,x){throw this.createError(a,c,u,x)},i.prototype.tolerateError=function(a,c,u,x){var d=this.createError(a,c,u,x);if(this.tolerant)this.recordError(d);else throw d},i}();e.ErrorHandler=r},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Messages={BadGetterArity:"Getter must not have any formal parameters",BadSetterArity:"Setter must have exactly one formal parameter",BadSetterRestParameter:"Setter function argument must not be a rest parameter",ConstructorIsAsync:"Class constructor may not be an async method",ConstructorSpecialMethod:"Class constructor may not be an accessor",DeclarationMissingInitializer:"Missing initializer in %0 declaration",DefaultRestParameter:"Unexpected token =",DuplicateBinding:"Duplicate binding %0",DuplicateConstructor:"A class may only have one constructor",DuplicateProtoProperty:"Duplicate __proto__ fields are not allowed in object literals",ForInOfLoopInitializer:"%0 loop variable declaration may not have an initializer",GeneratorInLegacyContext:"Generator declarations are not allowed in legacy contexts",IllegalBreak:"Illegal break statement",IllegalContinue:"Illegal continue statement",IllegalExportDeclaration:"Unexpected token",IllegalImportDeclaration:"Unexpected token",IllegalLanguageModeDirective:"Illegal 'use strict' directive in function with non-simple parameter list",IllegalReturn:"Illegal return statement",InvalidEscapedReservedWord:"Keyword must not contain escaped characters",InvalidHexEscapeSequence:"Invalid hexadecimal escape sequence",InvalidLHSInAssignment:"Invalid left-hand side in assignment",InvalidLHSInForIn:"Invalid left-hand side in for-in",InvalidLHSInForLoop:"Invalid left-hand side in for-loop",InvalidModuleSpecifier:"Unexpected token",InvalidRegExp:"Invalid regular expression",LetInLexicalBinding:"let is disallowed as a lexically bound name",MissingFromClause:"Unexpected token",MultipleDefaultsInSwitch:"More than one default clause in switch statement",NewlineAfterThrow:"Illegal newline after throw",NoAsAfterImportNamespace:"Unexpected token",NoCatchOrFinally:"Missing catch or finally after try",ParameterAfterRestParameter:"Rest parameter must be last formal parameter",Redeclaration:"%0 '%1' has already been declared",StaticPrototype:"Classes may not have static property named prototype",StrictCatchVariable:"Catch variable may not be eval or arguments in strict mode",StrictDelete:"Delete of an unqualified identifier in strict mode.",StrictFunction:"In strict mode code, functions can only be declared at top level or inside a block",StrictFunctionName:"Function name may not be eval or arguments in strict mode",StrictLHSAssignment:"Assignment to eval or arguments is not allowed in strict mode",StrictLHSPostfix:"Postfix increment/decrement may not have eval or arguments operand in strict mode",StrictLHSPrefix:"Prefix increment/decrement may not have eval or arguments operand in strict mode",StrictModeWith:"Strict mode code may not include a with statement",StrictOctalLiteral:"Octal literals are not allowed in strict mode.",StrictParamDupe:"Strict mode function may not have duplicate parameter names",StrictParamName:"Parameter name eval or arguments is not allowed in strict mode",StrictReservedWord:"Use of future reserved word in strict mode",StrictVarName:"Variable name may not be eval or arguments in strict mode",TemplateOctalLiteral:"Octal literals are not allowed in template strings.",UnexpectedEOS:"Unexpected end of input",UnexpectedIdentifier:"Unexpected identifier",UnexpectedNumber:"Unexpected number",UnexpectedReserved:"Unexpected reserved word",UnexpectedString:"Unexpected string",UnexpectedTemplate:"Unexpected quasi %0",UnexpectedToken:"Unexpected token %0",UnexpectedTokenIllegal:"Unexpected token ILLEGAL",UnknownLabel:"Undefined label '%0'",UnterminatedRegExp:"Invalid regular expression: missing /"}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(9),a=r(4),c=r(11);function u(p){return"0123456789abcdef".indexOf(p.toLowerCase())}function x(p){return"01234567".indexOf(p)}var d=function(){function p(h,m){this.source=h,this.errorHandler=m,this.trackComment=!1,this.isModule=!1,this.length=h.length,this.index=0,this.lineNumber=h.length>0?1:0,this.lineStart=0,this.curlyStack=[]}return p.prototype.saveState=function(){return{index:this.index,lineNumber:this.lineNumber,lineStart:this.lineStart}},p.prototype.restoreState=function(h){this.index=h.index,this.lineNumber=h.lineNumber,this.lineStart=h.lineStart},p.prototype.eof=function(){return this.index>=this.length},p.prototype.throwUnexpectedToken=function(h){return h===void 0&&(h=c.Messages.UnexpectedTokenIllegal),this.errorHandler.throwError(this.index,this.lineNumber,this.index-this.lineStart+1,h)},p.prototype.tolerateUnexpectedToken=function(h){h===void 0&&(h=c.Messages.UnexpectedTokenIllegal),this.errorHandler.tolerateError(this.index,this.lineNumber,this.index-this.lineStart+1,h)},p.prototype.skipSingleLineComment=function(h){var m=[],l,n;for(this.trackComment&&(m=[],l=this.index-h,n={start:{line:this.lineNumber,column:this.index-this.lineStart-h},end:{}});!this.eof();){var s=this.source.charCodeAt(this.index);if(++this.index,a.Character.isLineTerminator(s)){if(this.trackComment){n.end={line:this.lineNumber,column:this.index-this.lineStart-1};var o={multiLine:!1,slice:[l+h,this.index-1],range:[l,this.index-1],loc:n};m.push(o)}return s===13&&this.source.charCodeAt(this.index)===10&&++this.index,++this.lineNumber,this.lineStart=this.index,m}}if(this.trackComment){n.end={line:this.lineNumber,column:this.index-this.lineStart};var o={multiLine:!1,slice:[l+h,this.index],range:[l,this.index],loc:n};m.push(o)}return m},p.prototype.skipMultiLineComment=function(){var h=[],m,l;for(this.trackComment&&(h=[],m=this.index-2,l={start:{line:this.lineNumber,column:this.index-this.lineStart-2},end:{}});!this.eof();){var n=this.source.charCodeAt(this.index);if(a.Character.isLineTerminator(n))n===13&&this.source.charCodeAt(this.index+1)===10&&++this.index,++this.lineNumber,++this.index,this.lineStart=this.index;else if(n===42){if(this.source.charCodeAt(this.index+1)===47){if(this.index+=2,this.trackComment){l.end={line:this.lineNumber,column:this.index-this.lineStart};var s={multiLine:!0,slice:[m+2,this.index-2],range:[m,this.index],loc:l};h.push(s)}return h}++this.index}else++this.index}if(this.trackComment){l.end={line:this.lineNumber,column:this.index-this.lineStart};var s={multiLine:!0,slice:[m+2,this.index],range:[m,this.index],loc:l};h.push(s)}return this.tolerateUnexpectedToken(),h},p.prototype.scanComments=function(){var h;this.trackComment&&(h=[]);for(var m=this.index===0;!this.eof();){var l=this.source.charCodeAt(this.index);if(a.Character.isWhiteSpace(l))++this.index;else if(a.Character.isLineTerminator(l))++this.index,l===13&&this.source.charCodeAt(this.index)===10&&++this.index,++this.lineNumber,this.lineStart=this.index,m=!0;else if(l===47)if(l=this.source.charCodeAt(this.index+1),l===47){this.index+=2;var n=this.skipSingleLineComment(2);this.trackComment&&(h=h.concat(n)),m=!0}else if(l===42){this.index+=2;var n=this.skipMultiLineComment();this.trackComment&&(h=h.concat(n))}else break;else if(m&&l===45)if(this.source.charCodeAt(this.index+1)===45&&this.source.charCodeAt(this.index+2)===62){this.index+=3;var n=this.skipSingleLineComment(3);this.trackComment&&(h=h.concat(n))}else break;else if(l===60&&!this.isModule)if(this.source.slice(this.index+1,this.index+4)==="!--"){this.index+=4;var n=this.skipSingleLineComment(4);this.trackComment&&(h=h.concat(n))}else break;else break}return h},p.prototype.isFutureReservedWord=function(h){switch(h){case"enum":case"export":case"import":case"super":return!0;default:return!1}},p.prototype.isStrictModeReservedWord=function(h){switch(h){case"implements":case"interface":case"package":case"private":case"protected":case"public":case"static":case"yield":case"let":return!0;default:return!1}},p.prototype.isRestrictedWord=function(h){return h==="eval"||h==="arguments"},p.prototype.isKeyword=function(h){switch(h.length){case 2:return h==="if"||h==="in"||h==="do";case 3:return h==="var"||h==="for"||h==="new"||h==="try"||h==="let";case 4:return h==="this"||h==="else"||h==="case"||h==="void"||h==="with"||h==="enum";case 5:return h==="while"||h==="break"||h==="catch"||h==="throw"||h==="const"||h==="yield"||h==="class"||h==="super";case 6:return h==="return"||h==="typeof"||h==="delete"||h==="switch"||h==="export"||h==="import";case 7:return h==="default"||h==="finally"||h==="extends";case 8:return h==="function"||h==="continue"||h==="debugger";case 10:return h==="instanceof";default:return!1}},p.prototype.codePointAt=function(h){var m=this.source.charCodeAt(h);if(m>=55296&&m<=56319){var l=this.source.charCodeAt(h+1);if(l>=56320&&l<=57343){var n=m;m=(n-55296)*1024+l-56320+65536}}return m},p.prototype.scanHexEscape=function(h){for(var m=h==="u"?4:2,l=0,n=0;n1114111||h!=="}")&&this.throwUnexpectedToken(),a.Character.fromCodePoint(m)},p.prototype.getIdentifier=function(){for(var h=this.index++;!this.eof();){var m=this.source.charCodeAt(this.index);if(m===92)return this.index=h,this.getComplexIdentifier();if(m>=55296&&m<57343)return this.index=h,this.getComplexIdentifier();if(a.Character.isIdentifierPart(m))++this.index;else break}return this.source.slice(h,this.index)},p.prototype.getComplexIdentifier=function(){var h=this.codePointAt(this.index),m=a.Character.fromCodePoint(h);this.index+=m.length;var l;for(h===92&&(this.source.charCodeAt(this.index)!==117&&this.throwUnexpectedToken(),++this.index,this.source[this.index]==="{"?(++this.index,l=this.scanUnicodeCodePointEscape()):(l=this.scanHexEscape("u"),(l===null||l==="\\"||!a.Character.isIdentifierStart(l.charCodeAt(0)))&&this.throwUnexpectedToken()),m=l);!this.eof()&&(h=this.codePointAt(this.index),!!a.Character.isIdentifierPart(h));)l=a.Character.fromCodePoint(h),m+=l,this.index+=l.length,h===92&&(m=m.substr(0,m.length-1),this.source.charCodeAt(this.index)!==117&&this.throwUnexpectedToken(),++this.index,this.source[this.index]==="{"?(++this.index,l=this.scanUnicodeCodePointEscape()):(l=this.scanHexEscape("u"),(l===null||l==="\\"||!a.Character.isIdentifierPart(l.charCodeAt(0)))&&this.throwUnexpectedToken()),m+=l);return m},p.prototype.octalToDecimal=function(h){var m=h!=="0",l=x(h);return!this.eof()&&a.Character.isOctalDigit(this.source.charCodeAt(this.index))&&(m=!0,l=l*8+x(this.source[this.index++]),"0123".indexOf(h)>=0&&!this.eof()&&a.Character.isOctalDigit(this.source.charCodeAt(this.index))&&(l=l*8+x(this.source[this.index++]))),{code:l,octal:m}},p.prototype.scanIdentifier=function(){var h,m=this.index,l=this.source.charCodeAt(m)===92?this.getComplexIdentifier():this.getIdentifier();if(l.length===1?h=3:this.isKeyword(l)?h=4:l==="null"?h=5:l==="true"||l==="false"?h=1:h=3,h!==3&&m+l.length!==this.index){var n=this.index;this.index=m,this.tolerateUnexpectedToken(c.Messages.InvalidEscapedReservedWord),this.index=n}return{type:h,value:l,lineNumber:this.lineNumber,lineStart:this.lineStart,start:m,end:this.index}},p.prototype.scanPunctuator=function(){var h=this.index,m=this.source[this.index];switch(m){case"(":case"{":m==="{"&&this.curlyStack.push("{"),++this.index;break;case".":++this.index,this.source[this.index]==="."&&this.source[this.index+1]==="."&&(this.index+=2,m="...");break;case"}":++this.index,this.curlyStack.pop();break;case")":case";":case",":case"[":case"]":case":":case"?":case"~":++this.index;break;default:m=this.source.substr(this.index,4),m===">>>="?this.index+=4:(m=m.substr(0,3),m==="==="||m==="!=="||m===">>>"||m==="<<="||m===">>="||m==="**="?this.index+=3:(m=m.substr(0,2),m==="&&"||m==="||"||m==="=="||m==="!="||m==="+="||m==="-="||m==="*="||m==="/="||m==="++"||m==="--"||m==="<<"||m===">>"||m==="&="||m==="|="||m==="^="||m==="%="||m==="<="||m===">="||m==="=>"||m==="**"?this.index+=2:(m=this.source[this.index],"<>=!+-*%&|^/".indexOf(m)>=0&&++this.index)))}return this.index===h&&this.throwUnexpectedToken(),{type:7,value:m,lineNumber:this.lineNumber,lineStart:this.lineStart,start:h,end:this.index}},p.prototype.scanHexLiteral=function(h){for(var m="";!this.eof()&&a.Character.isHexDigit(this.source.charCodeAt(this.index));)m+=this.source[this.index++];return m.length===0&&this.throwUnexpectedToken(),a.Character.isIdentifierStart(this.source.charCodeAt(this.index))&&this.throwUnexpectedToken(),{type:6,value:parseInt("0x"+m,16),lineNumber:this.lineNumber,lineStart:this.lineStart,start:h,end:this.index}},p.prototype.scanBinaryLiteral=function(h){for(var m="",l;!this.eof()&&(l=this.source[this.index],!(l!=="0"&&l!=="1"));)m+=this.source[this.index++];return m.length===0&&this.throwUnexpectedToken(),this.eof()||(l=this.source.charCodeAt(this.index),(a.Character.isIdentifierStart(l)||a.Character.isDecimalDigit(l))&&this.throwUnexpectedToken()),{type:6,value:parseInt(m,2),lineNumber:this.lineNumber,lineStart:this.lineStart,start:h,end:this.index}},p.prototype.scanOctalLiteral=function(h,m){var l="",n=!1;for(a.Character.isOctalDigit(h.charCodeAt(0))?(n=!0,l="0"+this.source[this.index++]):++this.index;!this.eof()&&a.Character.isOctalDigit(this.source.charCodeAt(this.index));)l+=this.source[this.index++];return!n&&l.length===0&&this.throwUnexpectedToken(),(a.Character.isIdentifierStart(this.source.charCodeAt(this.index))||a.Character.isDecimalDigit(this.source.charCodeAt(this.index)))&&this.throwUnexpectedToken(),{type:6,value:parseInt(l,8),octal:n,lineNumber:this.lineNumber,lineStart:this.lineStart,start:m,end:this.index}},p.prototype.isImplicitOctalLiteral=function(){for(var h=this.index+1;h=0&&(n=n.replace(/\\u\{([0-9a-fA-F]+)\}|\\u([a-fA-F0-9]{4})/g,function(o,f,E){var g=parseInt(f||E,16);return g>1114111&&s.throwUnexpectedToken(c.Messages.InvalidRegExp),g<=65535?String.fromCharCode(g):l}).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,l));try{RegExp(n)}catch{this.throwUnexpectedToken(c.Messages.InvalidRegExp)}try{return new RegExp(h,m)}catch{return null}},p.prototype.scanRegExpBody=function(){var h=this.source[this.index];i.assert(h==="/","Regular expression literal must start with a slash");for(var m=this.source[this.index++],l=!1,n=!1;!this.eof();)if(h=this.source[this.index++],m+=h,h==="\\")h=this.source[this.index++],a.Character.isLineTerminator(h.charCodeAt(0))&&this.throwUnexpectedToken(c.Messages.UnterminatedRegExp),m+=h;else if(a.Character.isLineTerminator(h.charCodeAt(0)))this.throwUnexpectedToken(c.Messages.UnterminatedRegExp);else if(l)h==="]"&&(l=!1);else if(h==="/"){n=!0;break}else h==="["&&(l=!0);return n||this.throwUnexpectedToken(c.Messages.UnterminatedRegExp),m.substr(1,m.length-2)},p.prototype.scanRegExpFlags=function(){for(var h="",m="";!this.eof();){var l=this.source[this.index];if(!a.Character.isIdentifierPart(l.charCodeAt(0)))break;if(++this.index,l==="\\"&&!this.eof())if(l=this.source[this.index],l==="u"){++this.index;var n=this.index,s=this.scanHexEscape("u");if(s!==null)for(m+=s,h+="\\u";n=55296&&h<57343&&a.Character.isIdentifierStart(this.codePointAt(this.index))?this.scanIdentifier():this.scanPunctuator()},p}();e.Scanner=d},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TokenName={},e.TokenName[1]="Boolean",e.TokenName[2]="",e.TokenName[3]="Identifier",e.TokenName[4]="Keyword",e.TokenName[5]="Null",e.TokenName[6]="Numeric",e.TokenName[7]="Punctuator",e.TokenName[8]="String",e.TokenName[9]="RegularExpression",e.TokenName[10]="Template"},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.XHTMLEntities={quot:'"',amp:"&",apos:"'",gt:">",nbsp:"\xA0",iexcl:"\xA1",cent:"\xA2",pound:"\xA3",curren:"\xA4",yen:"\xA5",brvbar:"\xA6",sect:"\xA7",uml:"\xA8",copy:"\xA9",ordf:"\xAA",laquo:"\xAB",not:"\xAC",shy:"\xAD",reg:"\xAE",macr:"\xAF",deg:"\xB0",plusmn:"\xB1",sup2:"\xB2",sup3:"\xB3",acute:"\xB4",micro:"\xB5",para:"\xB6",middot:"\xB7",cedil:"\xB8",sup1:"\xB9",ordm:"\xBA",raquo:"\xBB",frac14:"\xBC",frac12:"\xBD",frac34:"\xBE",iquest:"\xBF",Agrave:"\xC0",Aacute:"\xC1",Acirc:"\xC2",Atilde:"\xC3",Auml:"\xC4",Aring:"\xC5",AElig:"\xC6",Ccedil:"\xC7",Egrave:"\xC8",Eacute:"\xC9",Ecirc:"\xCA",Euml:"\xCB",Igrave:"\xCC",Iacute:"\xCD",Icirc:"\xCE",Iuml:"\xCF",ETH:"\xD0",Ntilde:"\xD1",Ograve:"\xD2",Oacute:"\xD3",Ocirc:"\xD4",Otilde:"\xD5",Ouml:"\xD6",times:"\xD7",Oslash:"\xD8",Ugrave:"\xD9",Uacute:"\xDA",Ucirc:"\xDB",Uuml:"\xDC",Yacute:"\xDD",THORN:"\xDE",szlig:"\xDF",agrave:"\xE0",aacute:"\xE1",acirc:"\xE2",atilde:"\xE3",auml:"\xE4",aring:"\xE5",aelig:"\xE6",ccedil:"\xE7",egrave:"\xE8",eacute:"\xE9",ecirc:"\xEA",euml:"\xEB",igrave:"\xEC",iacute:"\xED",icirc:"\xEE",iuml:"\xEF",eth:"\xF0",ntilde:"\xF1",ograve:"\xF2",oacute:"\xF3",ocirc:"\xF4",otilde:"\xF5",ouml:"\xF6",divide:"\xF7",oslash:"\xF8",ugrave:"\xF9",uacute:"\xFA",ucirc:"\xFB",uuml:"\xFC",yacute:"\xFD",thorn:"\xFE",yuml:"\xFF",OElig:"\u0152",oelig:"\u0153",Scaron:"\u0160",scaron:"\u0161",Yuml:"\u0178",fnof:"\u0192",circ:"\u02C6",tilde:"\u02DC",Alpha:"\u0391",Beta:"\u0392",Gamma:"\u0393",Delta:"\u0394",Epsilon:"\u0395",Zeta:"\u0396",Eta:"\u0397",Theta:"\u0398",Iota:"\u0399",Kappa:"\u039A",Lambda:"\u039B",Mu:"\u039C",Nu:"\u039D",Xi:"\u039E",Omicron:"\u039F",Pi:"\u03A0",Rho:"\u03A1",Sigma:"\u03A3",Tau:"\u03A4",Upsilon:"\u03A5",Phi:"\u03A6",Chi:"\u03A7",Psi:"\u03A8",Omega:"\u03A9",alpha:"\u03B1",beta:"\u03B2",gamma:"\u03B3",delta:"\u03B4",epsilon:"\u03B5",zeta:"\u03B6",eta:"\u03B7",theta:"\u03B8",iota:"\u03B9",kappa:"\u03BA",lambda:"\u03BB",mu:"\u03BC",nu:"\u03BD",xi:"\u03BE",omicron:"\u03BF",pi:"\u03C0",rho:"\u03C1",sigmaf:"\u03C2",sigma:"\u03C3",tau:"\u03C4",upsilon:"\u03C5",phi:"\u03C6",chi:"\u03C7",psi:"\u03C8",omega:"\u03C9",thetasym:"\u03D1",upsih:"\u03D2",piv:"\u03D6",ensp:"\u2002",emsp:"\u2003",thinsp:"\u2009",zwnj:"\u200C",zwj:"\u200D",lrm:"\u200E",rlm:"\u200F",ndash:"\u2013",mdash:"\u2014",lsquo:"\u2018",rsquo:"\u2019",sbquo:"\u201A",ldquo:"\u201C",rdquo:"\u201D",bdquo:"\u201E",dagger:"\u2020",Dagger:"\u2021",bull:"\u2022",hellip:"\u2026",permil:"\u2030",prime:"\u2032",Prime:"\u2033",lsaquo:"\u2039",rsaquo:"\u203A",oline:"\u203E",frasl:"\u2044",euro:"\u20AC",image:"\u2111",weierp:"\u2118",real:"\u211C",trade:"\u2122",alefsym:"\u2135",larr:"\u2190",uarr:"\u2191",rarr:"\u2192",darr:"\u2193",harr:"\u2194",crarr:"\u21B5",lArr:"\u21D0",uArr:"\u21D1",rArr:"\u21D2",dArr:"\u21D3",hArr:"\u21D4",forall:"\u2200",part:"\u2202",exist:"\u2203",empty:"\u2205",nabla:"\u2207",isin:"\u2208",notin:"\u2209",ni:"\u220B",prod:"\u220F",sum:"\u2211",minus:"\u2212",lowast:"\u2217",radic:"\u221A",prop:"\u221D",infin:"\u221E",ang:"\u2220",and:"\u2227",or:"\u2228",cap:"\u2229",cup:"\u222A",int:"\u222B",there4:"\u2234",sim:"\u223C",cong:"\u2245",asymp:"\u2248",ne:"\u2260",equiv:"\u2261",le:"\u2264",ge:"\u2265",sub:"\u2282",sup:"\u2283",nsub:"\u2284",sube:"\u2286",supe:"\u2287",oplus:"\u2295",otimes:"\u2297",perp:"\u22A5",sdot:"\u22C5",lceil:"\u2308",rceil:"\u2309",lfloor:"\u230A",rfloor:"\u230B",loz:"\u25CA",spades:"\u2660",clubs:"\u2663",hearts:"\u2665",diams:"\u2666",lang:"\u27E8",rang:"\u27E9"}},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=r(10),a=r(12),c=r(13),u=function(){function d(){this.values=[],this.curly=this.paren=-1}return d.prototype.beforeFunctionExpression=function(p){return["(","{","[","in","typeof","instanceof","new","return","case","delete","throw","void","=","+=","-=","*=","**=","/=","%=","<<=",">>=",">>>=","&=","|=","^=",",","+","-","*","**","/","%","++","--","<<",">>",">>>","&","|","^","!","~","&&","||","?",":","===","==",">=","<=","<",">","!=","!=="].indexOf(p)>=0},d.prototype.isRegexStart=function(){var p=this.values[this.values.length-1],h=p!==null;switch(p){case"this":case"]":h=!1;break;case")":var m=this.values[this.paren-1];h=m==="if"||m==="while"||m==="for"||m==="with";break;case"}":if(h=!1,this.values[this.curly-3]==="function"){var l=this.values[this.curly-4];h=l?!this.beforeFunctionExpression(l):!1}else if(this.values[this.curly-4]==="function"){var l=this.values[this.curly-5];h=l?!this.beforeFunctionExpression(l):!0}break;default:break}return h},d.prototype.push=function(p){p.type===7||p.type===4?(p.value==="{"?this.curly=this.values.length:p.value==="("&&(this.paren=this.values.length),this.values.push(p.value)):this.values.push(null)},d}(),x=function(){function d(p,h){this.errorHandler=new i.ErrorHandler,this.errorHandler.tolerant=h?typeof h.tolerant=="boolean"&&h.tolerant:!1,this.scanner=new a.Scanner(p,this.errorHandler),this.scanner.trackComment=h?typeof h.comment=="boolean"&&h.comment:!1,this.trackRange=h?typeof h.range=="boolean"&&h.range:!1,this.trackLoc=h?typeof h.loc=="boolean"&&h.loc:!1,this.buffer=[],this.reader=new u}return d.prototype.errors=function(){return this.errorHandler.errors},d.prototype.getNextToken=function(){if(this.buffer.length===0){var p=this.scanner.scanComments();if(this.scanner.trackComment)for(var h=0;h{function Bl(t){return Array.isArray?Array.isArray(t):Pt(t)==="[object Array]"}Z.isArray=Bl;function Pl(t){return typeof t=="boolean"}Z.isBoolean=Pl;function Rl(t){return t===null}Z.isNull=Rl;function Il(t){return t==null}Z.isNullOrUndefined=Il;function Nl(t){return typeof t=="number"}Z.isNumber=Nl;function Ll(t){return typeof t=="string"}Z.isString=Ll;function Ol(t){return typeof t=="symbol"}Z.isSymbol=Ol;function Ml(t){return t===void 0}Z.isUndefined=Ml;function Hl(t){return Pt(t)==="[object RegExp]"}Z.isRegExp=Hl;function Xl(t){return typeof t=="object"&&t!==null}Z.isObject=Xl;function Ul(t){return Pt(t)==="[object Date]"}Z.isDate=Ul;function $l(t){return Pt(t)==="[object Error]"||t instanceof Error}Z.isError=$l;function jl(t){return typeof t=="function"}Z.isFunction=jl;function Jl(t){return t===null||typeof t=="boolean"||typeof t=="number"||typeof t=="string"||typeof t=="symbol"||typeof t>"u"}Z.isPrimitive=Jl;Z.isBuffer=O("buffer").Buffer.isBuffer;function Pt(t){return Object.prototype.toString.call(t)}});var Ds=_((tx,gs)=>{var fs=[1,10,100,1e3,1e4,1e5,1e6,1e7,1e8,1e9],I,ps=t=>t<1e5?t<100?t<10?0:1:t<1e4?t<1e3?2:3:4:t<1e7?t<1e6?5:6:t<1e9?t<1e8?7:8:9;function ds(t,e){if(t===e)return 0;if(~~t===t&&~~e===e){if(t===0||e===0)return t=0)return-1;if(t>=0)return 1;t=-t,e=-e}let a=ps(t),c=ps(e),u=0;return ac&&(e*=fs[a-c-1],t/=10,u=1),t===e?u:t=32;)e|=t&1,t>>=1;return t+e}function ms(t,e,r,i){let a=e+1;if(a===r)return 1;if(i(t[a++],t[e])<0){for(;a=0;)a++;return a-e}function xs(t,e,r){for(r--;e>>1;a(c,t[h])<0?d=h:x=h+1}let p=i-x;switch(p){case 3:t[x+3]=t[x+2],I[x+3]=I[x+2];case 2:t[x+2]=t[x+1],I[x+2]=I[x+1];case 1:t[x+1]=t[x],I[x+1]=I[x];break;default:for(;p>0;)t[x+p]=t[x+p-1],I[x+p]=I[x+p-1],p--}t[x]=c,I[x]=u}}function _r(t,e,r,i,a,c){let u=0,x=0,d=1;if(c(t,e[r+a])>0){for(x=i-a;d0;)u=d,d=(d<<1)+1,d<=0&&(d=x);d>x&&(d=x),u+=a,d+=a}else{for(x=a+1;dx&&(d=x);let p=u;u=a-d,d=a-p}for(u++;u>>1);c(t,e[r+p])>0?u=p+1:d=p}return d}function kr(t,e,r,i,a,c){let u=0,x=0,d=1;if(c(t,e[r+a])<0){for(x=a+1;dx&&(d=x);let p=u;u=a-d,d=a-p}else{for(x=i-a;d=0;)u=d,d=(d<<1)+1,d<=0&&(d=x);d>x&&(d=x),u+=a,d+=a}for(u++;u>>1);c(t,e[r+p])<0?d=p:u=p+1}return d}var Tr=class{constructor(e,r){this.array=e,this.compare=r;let{length:i}=e;this.length=i,this.minGallop=7,this.tmpStorageLength=i<2*256?i>>>1:256,this.tmp=new Array(this.tmpStorageLength),this.tmpIndex=new Array(this.tmpStorageLength),this.stackLength=i<120?5:i<1542?10:i<119151?19:40,this.runStart=new Array(this.stackLength),this.runLength=new Array(this.stackLength),this.stackSize=0}pushRun(e,r){this.runStart[this.stackSize]=e,this.runLength[this.stackSize]=r,this.stackSize+=1}mergeRuns(){for(;this.stackSize>1;){let e=this.stackSize-2;if(e>=1&&this.runLength[e-1]<=this.runLength[e]+this.runLength[e+1]||e>=2&&this.runLength[e-2]<=this.runLength[e]+this.runLength[e-1])this.runLength[e-1]this.runLength[e+1])break;this.mergeAt(e)}}forceMergeRuns(){for(;this.stackSize>1;){let e=this.stackSize-2;e>0&&this.runLength[e-1]=7||o>=7);if(f)break;n<0&&(n=0),n+=2}if(this.minGallop=n,n<1&&(this.minGallop=1),r===1){for(p=0;p=0;p--)u[s+p]=u[n+p],I[s+p]=I[n+p];u[l]=x[m],I[l]=d[m];return}let{minGallop:o}=this;for(;;){let f=0,E=0,g=!1;do if(c(x[m],u[h])<0){if(u[l]=u[h],I[l]=I[h],l--,h--,f++,E=0,--r===0){g=!0;break}}else if(u[l]=x[m],I[l]=d[m],l--,m--,E++,f=0,--a===1){g=!0;break}while((f|E)=0;p--)u[s+p]=u[n+p],I[s+p]=I[n+p];if(r===0){g=!0;break}}if(u[l]=x[m],I[l]=d[m],l--,m--,--a===1){g=!0;break}if(E=a-_r(u[h],x,0,a,a-1,c),E!==0){for(l-=E,m-=E,a-=E,s=l+1,n=m+1,p=0;p=7||E>=7);if(g)break;o<0&&(o=0),o+=2}if(this.minGallop=o,o<1&&(this.minGallop=1),a===1){for(l-=r,h-=r,s=l+1,n=h+1,p=r-1;p>=0;p--)u[s+p]=u[n+p],I[s+p]=I[n+p];u[l]=x[m],I[l]=d[m]}else{if(a===0)throw new Error("mergeHigh preconditions were not respected");for(n=l-(a-1),p=0;pp&&(h=p),Es(t,r,r+h,r+x,e),x=h}d.pushRun(r,x),d.mergeRuns(),u-=x,r+=x}while(u!==0);return d.forceMergeRuns(),I}gs.exports={sort:ql}});var vs=_((rx,ys)=>{"use strict";var Gl=Object.prototype.hasOwnProperty;ys.exports=(t,e)=>Gl.call(t,e)});var xt=_((ix,Ps)=>{var Rr=vs(),{isObject:Ss,isArray:Kl,isString:Wl,isNumber:Vl}=Rt(),Ir="before",Cs="after-prop",bs="after-colon",Fs="after-value",ws="after",_s="before-all",ks="after-all",Yl="[",Ql="]",Zl="{",eh="}",th=",",rh="",ih="-",Nr=[Ir,Cs,bs,Fs,ws],nh=[Ir,_s,ks].map(Symbol.for),Ts=":",As=void 0,mt=(t,e)=>Symbol.for(t+Ts+e),It=(t,e,r)=>Object.defineProperty(t,e,{value:r,writable:!0,configurable:!0}),Pr=(t,e,r,i,a,c)=>{let u=mt(a,i);if(!Rr(e,u))return;let x=r===i?u:mt(a,r);It(t,x,e[u]),c&&delete e[u]},Bs=(t,e,r,i,a)=>{Nr.forEach(c=>{Pr(t,e,r,i,c,a)})},sh=(t,e,r)=>{e!==r&&Nr.forEach(i=>{let a=mt(i,r);if(!Rr(t,a)){Pr(t,t,r,e,i,!0);return}let c=t[a];delete t[a],Pr(t,t,r,e,i,!0),It(t,mt(i,e),c)})},Br=(t,e)=>{nh.forEach(r=>{let i=e[r];i&&It(t,r,i)})},ah=(t,e,r)=>(r.forEach(i=>{!Wl(i)&&!Vl(i)||Rr(e,i)&&(t[i]=e[i],Bs(t,e,i,i))}),t);Ps.exports={SYMBOL_PREFIXES:Nr,PREFIX_BEFORE:Ir,PREFIX_AFTER_PROP:Cs,PREFIX_AFTER_COLON:bs,PREFIX_AFTER_VALUE:Fs,PREFIX_AFTER:ws,PREFIX_BEFORE_ALL:_s,PREFIX_AFTER_ALL:ks,BRACKET_OPEN:Yl,BRACKET_CLOSE:Ql,CURLY_BRACKET_OPEN:Zl,CURLY_BRACKET_CLOSE:eh,COLON:Ts,COMMA:th,MINUS:ih,EMPTY:rh,UNDEFINED:As,symbol:mt,define:It,copy_comments:Bs,swap_comments:sh,assign_non_prop_comments:Br,assign(t,e,r){if(!Ss(t))throw new TypeError("Cannot convert undefined or null to object");if(!Ss(e))return t;if(r===As)r=Object.keys(e),Br(t,e);else if(Kl(r))r.length===0&&Br(t,e);else throw new TypeError("keys must be array or undefined");return ah(t,e,r)}}});var Or=_((nx,Os)=>{var{isArray:oh}=Rt(),{sort:uh}=Ds(),{SYMBOL_PREFIXES:ch,UNDEFINED:Rs,symbol:lh,copy_comments:hh,swap_comments:Ls}=xt(),fh=t=>{let{length:e}=t,r=0,i=e/2;for(;r{hh(t,e,r+i,r,a)},tt=(t,e,r,i,a,c)=>{if(a>0){let x=i;for(;x-- >0;)Is(t,e,r+x,a,c);return}let u=0;for(;u{ch.forEach(r=>{let i=lh(r,e);delete t[i]})},ph=(t,e)=>{let r=e;for(;r in t;)r=t[r];return r},Lr=class t extends Array{splice(...e){let{length:r}=this,i=super.splice(...e),[a,c,...u]=e;a<0&&(a+=r),arguments.length===1?c=r-a:c=Math.min(r-a,c);let{length:x}=u,d=x-c,p=a+c,h=r-p;return tt(this,this,p,h,d,!0),i}slice(...e){let{length:r}=this,i=super.slice(...e);if(!i.length)return new t;let[a,c]=e;return c===Rs?c=r:c<0&&(c+=r),a<0?a+=r:a===Rs&&(a=0),tt(i,this,a,c-a,-a),i}unshift(...e){let{length:r}=this,i=super.unshift(...e),{length:a}=e;return a>0&&tt(this,this,0,r,a,!0),i}shift(){let e=super.shift(),{length:r}=this;return Ns(this,0),tt(this,this,1,r,-1,!0),e}reverse(){return super.reverse(),fh(this),this}pop(){let e=super.pop();return Ns(this,this.length),e}concat(...e){let{length:r}=this,i=super.concat(...e);return e.length&&(tt(i,this,0,this.length,0),e.forEach(a=>{let c=r;r+=oh(a)?a.length:1,a instanceof t&&tt(i,a,0,a.length,c)})),i}sort(...e){let r=uh(this,...e.slice(0,1)),i=Object.create(null);return r.forEach((a,c)=>{if(a===c)return;let u=ph(i,a);u!==c&&(i[c]=u,Ls(this,c,u))}),this}};Os.exports={CommentArray:Lr}});var ea=_((sx,Zs)=>{var dh=hs(),{CommentArray:mh}=Or(),{PREFIX_BEFORE:Lt,PREFIX_AFTER_PROP:xh,PREFIX_AFTER_COLON:Eh,PREFIX_AFTER_VALUE:Us,PREFIX_AFTER:Hr,PREFIX_BEFORE_ALL:gh,PREFIX_AFTER_ALL:Dh,BRACKET_OPEN:yh,BRACKET_CLOSE:Ms,CURLY_BRACKET_OPEN:vh,CURLY_BRACKET_CLOSE:Hs,COLON:$s,COMMA:js,MINUS:Xs,EMPTY:Sh,UNDEFINED:Ht,define:Xr,assign_non_prop_comments:Ah}=xt(),Js=t=>dh.tokenize(t,{comment:!0,loc:!0}),Ur=[],Re=null,ve=null,$r=[],Ie,zs=!1,qs=!1,Et=null,gt=null,ee=null,Gs,Ot=null,Ks=()=>{$r.length=Ur.length=0,gt=null,Ie=Ht},Ch=()=>{Ks(),Et.length=0,ve=Re=Et=gt=ee=Ot=null},jr=t=>Symbol.for(Ie!==Ht?t+$s+Ie:t),Jr=(t,e)=>Ot?Ot(t,e):e,Ws=()=>{let t=new SyntaxError(`Unexpected token ${ee.value.slice(0,1)}`);throw Object.assign(t,ee.loc.start),t},Vs=()=>{let t=new SyntaxError("Unexpected end of JSON input");throw Object.assign(t,gt?gt.loc.end:{line:1,column:0}),t},pe=()=>{let t=Et[++Gs];qs=ee&&t&&ee.loc.end.line===t.loc.start.line||!1,gt=ee,ee=t},Mr=()=>(ee||Vs(),ee.type==="Punctuator"?ee.value:ee.type),je=t=>Mr()===t,Nt=t=>{je(t)||Ws()},zr=t=>{Ur.push(Re),Re=t},qr=()=>{Re=Ur.pop()},Ys=()=>{if(!ve)return;let t=[];for(let r of ve)if(r.inline)t.push(r);else break;let{length:e}=t;e&&(e===ve.length?ve=null:ve.splice(0,e),Xr(Re,jr(Hr),t))},Pe=t=>{ve&&(Xr(Re,jr(t),ve),ve=null)},Se=t=>{let e=[];for(;ee&&(je("LineComment")||je("BlockComment"));){let r={...ee,inline:qs};e.push(r),pe()}if(!zs&&e.length){if(t){Xr(Re,jr(t),e);return}ve=e}},Mt=(t,e)=>{e&&$r.push(Ie),Ie=t},Qs=()=>{Ie=$r.pop()},bh=()=>{let t={};zr(t),Mt(Ht,!0);let e=!1,r;for(Se();!je(Hs)&&!(e&&(Pe(Us),Nt(js),pe(),Se(),Ys(),je(Hs)));)e=!0,Nt("String"),r=JSON.parse(ee.value),Mt(r),Pe(Lt),pe(),Se(xh),Nt($s),pe(),Se(Eh),t[r]=Jr(r,Gr()),Se();return e&&Pe(Hr),pe(),Ie=void 0,e||Pe(Lt),qr(),Qs(),t},Fh=()=>{let t=new mh;zr(t),Mt(Ht,!0);let e=!1,r=0;for(Se();!je(Ms)&&!(e&&(Pe(Us),Nt(js),pe(),Se(),Ys(),je(Ms)));)e=!0,Mt(r),Pe(Lt),t[r]=Jr(r,Gr()),r++,Se();return e&&Pe(Hr),pe(),Ie=void 0,e||Pe(Lt),qr(),Qs(),t};function Gr(){let t=Mr();if(t===vh)return pe(),bh();if(t===yh)return pe(),Fh();let e=Sh;t===Xs&&(pe(),t=Mr(),e=Xs);let r;switch(t){case"String":case"Boolean":case"Null":case"Numeric":return r=ee.value,pe(),JSON.parse(e+r);default:}}var wh=t=>Object(t)===t,_h=(t,e,r)=>{Ks(),Et=Js(t),Ot=e,zs=r,Et.length||Vs(),Gs=-1,pe(),zr({}),Se(gh);let i=Gr();return Se(Dh),ee&&Ws(),!r&&i!==null&&(wh(i)||(i=new Object(i)),Ah(i,Re)),qr(),i=Jr("",i),Ch(),i};Zs.exports={parse:_h,tokenize:Js}});var ra=_((ax,ta)=>{"use strict";var _e="",Kr;ta.exports=kh;function kh(t,e){if(typeof t!="string")throw new TypeError("expected a string");if(e===1)return t;if(e===2)return t+t;var r=t.length*e;if(Kr!==t||typeof Kr>"u")Kr=t,_e="";else if(_e.length>=r)return _e.substr(0,r);for(;r>_e.length&&e>1;)e&1&&(_e+=t),e>>=1,t+=t;return _e+=t,_e=_e.substr(0,r),_e}});var pa=_((ox,fa)=>{var{isArray:Yr,isObject:ia,isFunction:Vr,isNumber:Th,isString:Bh}=Rt(),Ph=ra(),{PREFIX_BEFORE_ALL:Rh,PREFIX_BEFORE:na,PREFIX_AFTER_PROP:Ih,PREFIX_AFTER_COLON:Nh,PREFIX_AFTER_VALUE:Lh,PREFIX_AFTER:Qr,PREFIX_AFTER_ALL:Oh,BRACKET_OPEN:Mh,BRACKET_CLOSE:Hh,CURLY_BRACKET_OPEN:Xh,CURLY_BRACKET_CLOSE:Uh,COLON:$h,COMMA:sa,EMPTY:ue,UNDEFINED:jh}=xt(),Wr=/[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,Zr=" ",Je=` +`,aa="null",oa=t=>`${na}:${t}`,Jh=t=>`${Ih}:${t}`,zh=t=>`${Nh}:${t}`,ua=t=>`${Lh}:${t}`,ca=t=>`${Qr}:${t}`,qh={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},Gh=t=>(Wr.lastIndex=0,Wr.test(t)?t.replace(Wr,e=>{let r=qh[e];return typeof r=="string"?r:e}):t),la=t=>`"${Gh(t)}"`,Kh=(t,e)=>e?`//${t}`:`/*${t}*/`,oe=(t,e,r,i)=>{let a=t[Symbol.for(e)];if(!a||!a.length)return ue;let c=!1,u=a.reduce((x,{inline:d,type:p,value:h})=>{let m=d?Zr:Je+r;return c=p==="LineComment",x+m+Kh(h,c)},ue);return i||c?u+Je+r:u},rt=null,yt=ue,Wh=()=>{rt=null,yt=ue},Dt=(t,e,r)=>t?e?t+e.trim()+Je+r:t.trimRight()+Je+r:e?e.trimRight()+Je+r:ue,ha=(t,e,r)=>{let i=oe(e,na,r+yt,!0);return Dt(i,t,r)},Vh=(t,e)=>{let r=e+yt,{length:i}=t,a=ue,c=ue;for(let u=0;u{if(!t)return"null";let r=e+yt,i=ue,a=ue,c=!0,u=Yr(rt)?rt:Object.keys(t),x=d=>{let p=ei(d,t,r);if(p===jh)return;c||(i+=sa),c=!1;let h=Dt(a,oe(t,oa(d),r),r);i+=h||Je+r,i+=la(d)+oe(t,Jh(d),r)+$h+oe(t,zh(d),r)+Zr+p+oe(t,ua(d),r),a=oe(t,ca(d),r)};return u.forEach(x),i+=Dt(a,oe(t,Qr,r),r),Xh+ha(i,t,e)+Uh};function ei(t,e,r){let i=e[t];switch(ia(i)&&Vr(i.toJSON)&&(i=i.toJSON(t)),Vr(rt)&&(i=rt.call(e,t,i)),typeof i){case"string":return la(i);case"number":return Number.isFinite(i)?String(i):aa;case"boolean":case"null":return String(i);case"object":return Yr(i)?Vh(i,r):Yh(i,r);default:}}var Qh=t=>Bh(t)?t:Th(t)?Ph(Zr,t):ue,{toString:Zh}=Object.prototype,ef=["[object Number]","[object String]","[object Boolean]"],tf=t=>{if(typeof t!="object")return!1;let e=Zh.call(t);return ef.includes(e)};fa.exports=(t,e,r)=>{let i=Qh(r);if(!i)return JSON.stringify(t,e);!Vr(e)&&!Yr(e)&&(e=null),rt=e,yt=i;let a=tf(t)?JSON.stringify(t):ei("",{"":t},ue);return Wh(),ia(t)?oe(t,Rh,ue).trimLeft()+a+oe(t,Oh,ue).trimRight():a}});var ti=_((ux,da)=>{var{parse:rf,tokenize:nf}=ea(),sf=pa(),{CommentArray:af}=Or(),{assign:of}=xt();da.exports={parse:rf,stringify:sf,tokenize:nf,CommentArray:af,assign:of}});var Sa=_(it=>{"use strict";Object.defineProperty(it,"__esModule",{value:!0});it.splitWhen=it.flatten=void 0;function Ef(t){return t.reduce((e,r)=>[].concat(e,r),[])}it.flatten=Ef;function gf(t,e){let r=[[]],i=0;for(let a of t)e(a)?(i++,r[i]=[]):r[i].push(a);return r}it.splitWhen=gf});var Aa=_($t=>{"use strict";Object.defineProperty($t,"__esModule",{value:!0});$t.isEnoentCodeError=void 0;function Df(t){return t.code==="ENOENT"}$t.isEnoentCodeError=Df});var Ca=_(jt=>{"use strict";Object.defineProperty(jt,"__esModule",{value:!0});jt.createDirentFromStats=void 0;var oi=class{constructor(e,r){this.name=e,this.isBlockDevice=r.isBlockDevice.bind(r),this.isCharacterDevice=r.isCharacterDevice.bind(r),this.isDirectory=r.isDirectory.bind(r),this.isFIFO=r.isFIFO.bind(r),this.isFile=r.isFile.bind(r),this.isSocket=r.isSocket.bind(r),this.isSymbolicLink=r.isSymbolicLink.bind(r)}};function yf(t,e){return new oi(t,e)}jt.createDirentFromStats=yf});var _a=_(V=>{"use strict";Object.defineProperty(V,"__esModule",{value:!0});V.convertPosixPathToPattern=V.convertWindowsPathToPattern=V.convertPathToPattern=V.escapePosixPath=V.escapeWindowsPath=V.escape=V.removeLeadingDotSegment=V.makeAbsolute=V.unixify=void 0;var vf=O("os"),Sf=O("path"),ba=vf.platform()==="win32",Af=2,Cf=/(\\?)([()*?[\]{|}]|^!|[!+@](?=\()|\\(?![!()*+?@[\]{|}]))/g,bf=/(\\?)([()[\]{}]|^!|[!+@](?=\())/g,Ff=/^\\\\([.?])/,wf=/\\(?![!()+@[\]{}])/g;function _f(t){return t.replace(/\\/g,"/")}V.unixify=_f;function kf(t,e){return Sf.resolve(t,e)}V.makeAbsolute=kf;function Tf(t){if(t.charAt(0)==="."){let e=t.charAt(1);if(e==="/"||e==="\\")return t.slice(Af)}return t}V.removeLeadingDotSegment=Tf;V.escape=ba?ui:ci;function ui(t){return t.replace(bf,"\\$2")}V.escapeWindowsPath=ui;function ci(t){return t.replace(Cf,"\\$2")}V.escapePosixPath=ci;V.convertPathToPattern=ba?Fa:wa;function Fa(t){return ui(t).replace(Ff,"//$1").replace(wf,"/")}V.convertWindowsPathToPattern=Fa;function wa(t){return ci(t)}V.convertPosixPathToPattern=wa});var Ta=_((dx,ka)=>{ka.exports=function(e){if(typeof e!="string"||e==="")return!1;for(var r;r=/(\\).|([@?!+*]\(.*\))/g.exec(e);){if(r[2])return!0;e=e.slice(r.index+r[0].length)}return!1}});var Ra=_((mx,Pa)=>{var Bf=Ta(),Ba={"{":"}","(":")","[":"]"},Pf=function(t){if(t[0]==="!")return!0;for(var e=0,r=-2,i=-2,a=-2,c=-2,u=-2;ee&&(u===-1||u>i||(u=t.indexOf("\\",e),u===-1||u>i)))||a!==-1&&t[e]==="{"&&t[e+1]!=="}"&&(a=t.indexOf("}",e),a>e&&(u=t.indexOf("\\",e),u===-1||u>a))||c!==-1&&t[e]==="("&&t[e+1]==="?"&&/[:!=]/.test(t[e+2])&&t[e+3]!==")"&&(c=t.indexOf(")",e),c>e&&(u=t.indexOf("\\",e),u===-1||u>c))||r!==-1&&t[e]==="("&&t[e+1]!=="|"&&(rr&&(u=t.indexOf("\\",r),u===-1||u>c))))return!0;if(t[e]==="\\"){var x=t[e+1];e+=2;var d=Ba[x];if(d){var p=t.indexOf(d,e);p!==-1&&(e=p+1)}if(t[e]==="!")return!0}else e++}return!1},Rf=function(t){if(t[0]==="!")return!0;for(var e=0;e{"use strict";var If=Ra(),Nf=O("path").posix.dirname,Lf=O("os").platform()==="win32",li="/",Of=/\\/g,Mf=/[\{\[].*[\}\]]$/,Hf=/(^|[^\\])([\{\[]|\([^\)]+$)/,Xf=/\\([\!\*\?\|\[\]\(\)\{\}])/g;Ia.exports=function(e,r){var i=Object.assign({flipBackslashes:!0},r);i.flipBackslashes&&Lf&&e.indexOf(li)<0&&(e=e.replace(Of,li)),Mf.test(e)&&(e+=li),e+="a";do e=Nf(e);while(If(e)||Hf.test(e));return e.replace(Xf,"$1")}});var Jt=_(de=>{"use strict";de.isInteger=t=>typeof t=="number"?Number.isInteger(t):typeof t=="string"&&t.trim()!==""?Number.isInteger(Number(t)):!1;de.find=(t,e)=>t.nodes.find(r=>r.type===e);de.exceedsLimit=(t,e,r=1,i)=>i===!1||!de.isInteger(t)||!de.isInteger(e)?!1:(Number(e)-Number(t))/Number(r)>=i;de.escapeNode=(t,e=0,r)=>{let i=t.nodes[e];i&&(r&&i.type===r||i.type==="open"||i.type==="close")&&i.escaped!==!0&&(i.value="\\"+i.value,i.escaped=!0)};de.encloseBrace=t=>t.type!=="brace"||t.commas>>0+t.ranges>>0?!1:(t.invalid=!0,!0);de.isInvalidBrace=t=>t.type!=="brace"?!1:t.invalid===!0||t.dollar?!0:!(t.commas>>0+t.ranges>>0)||t.open!==!0||t.close!==!0?(t.invalid=!0,!0):!1;de.isOpenOrClose=t=>t.type==="open"||t.type==="close"?!0:t.open===!0||t.close===!0;de.reduce=t=>t.reduce((e,r)=>(r.type==="text"&&e.push(r.value),r.type==="range"&&(r.type="text"),e),[]);de.flatten=(...t)=>{let e=[],r=i=>{for(let a=0;a{"use strict";var La=Jt();Oa.exports=(t,e={})=>{let r=(i,a={})=>{let c=e.escapeInvalid&&La.isInvalidBrace(a),u=i.invalid===!0&&e.escapeInvalid===!0,x="";if(i.value)return(c||u)&&La.isOpenOrClose(i)?"\\"+i.value:i.value;if(i.value)return i.value;if(i.nodes)for(let d of i.nodes)x+=r(d);return x};return r(t)}});var Ha=_((Dx,Ma)=>{"use strict";Ma.exports=function(t){return typeof t=="number"?t-t===0:typeof t=="string"&&t.trim()!==""?Number.isFinite?Number.isFinite(+t):isFinite(+t):!1}});var Ka=_((yx,Ga)=>{"use strict";var Xa=Ha(),ze=(t,e,r)=>{if(Xa(t)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(e===void 0||t===e)return String(t);if(Xa(e)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let i={relaxZeros:!0,...r};typeof i.strictZeros=="boolean"&&(i.relaxZeros=i.strictZeros===!1);let a=String(i.relaxZeros),c=String(i.shorthand),u=String(i.capture),x=String(i.wrap),d=t+":"+e+"="+a+c+u+x;if(ze.cache.hasOwnProperty(d))return ze.cache[d].result;let p=Math.min(t,e),h=Math.max(t,e);if(Math.abs(p-h)===1){let o=t+"|"+e;return i.capture?`(${o})`:i.wrap===!1?o:`(?:${o})`}let m=qa(t)||qa(e),l={min:t,max:e,a:p,b:h},n=[],s=[];if(m&&(l.isPadded=m,l.maxLen=String(l.max).length),p<0){let o=h<0?Math.abs(h):1;s=Ua(o,Math.abs(p),l,i),p=l.a=0}return h>=0&&(n=Ua(p,h,l,i)),l.negatives=s,l.positives=n,l.result=Uf(s,n,i),i.capture===!0?l.result=`(${l.result})`:i.wrap!==!1&&n.length+s.length>1&&(l.result=`(?:${l.result})`),ze.cache[d]=l,l.result};function Uf(t,e,r){let i=hi(t,e,"-",!1,r)||[],a=hi(e,t,"",!1,r)||[],c=hi(t,e,"-?",!0,r)||[];return i.concat(c).concat(a).join("|")}function $f(t,e){let r=1,i=1,a=ja(t,r),c=new Set([e]);for(;t<=a&&a<=e;)c.add(a),r+=1,a=ja(t,r);for(a=Ja(e+1,i)-1;t1&&x.count.pop(),x.count.push(h.count[0]),x.string=x.pattern+za(x.count),u=p+1;continue}r.isPadded&&(m=Gf(p,r,i)),h.string=m+h.pattern+za(h.count),c.push(h),u=p+1,x=h}return c}function hi(t,e,r,i,a){let c=[];for(let u of t){let{string:x}=u;!i&&!$a(e,"string",x)&&c.push(r+x),i&&$a(e,"string",x)&&c.push(r+x)}return c}function Jf(t,e){let r=[];for(let i=0;ie?1:e>t?-1:0}function $a(t,e,r){return t.some(i=>i[e]===r)}function ja(t,e){return Number(String(t).slice(0,-e)+"9".repeat(e))}function Ja(t,e){return t-t%Math.pow(10,e)}function za(t){let[e=0,r=""]=t;return r||e>1?`{${e+(r?","+r:"")}}`:""}function qf(t,e,r){return`[${t}${e-t===1?"":"-"}${e}]`}function qa(t){return/^-?(0+)\d/.test(t)}function Gf(t,e,r){if(!e.isPadded)return t;let i=Math.abs(e.maxLen-String(t).length),a=r.relaxZeros!==!1;switch(i){case 0:return"";case 1:return a?"0?":"0";case 2:return a?"0{0,2}":"00";default:return a?`0{0,${i}}`:`0{${i}}`}}ze.cache={};ze.clearCache=()=>ze.cache={};Ga.exports=ze});var di=_((vx,to)=>{"use strict";var Kf=O("util"),Va=Ka(),Wa=t=>t!==null&&typeof t=="object"&&!Array.isArray(t),Wf=t=>e=>t===!0?Number(e):String(e),fi=t=>typeof t=="number"||typeof t=="string"&&t!=="",vt=t=>Number.isInteger(+t),pi=t=>{let e=`${t}`,r=-1;if(e[0]==="-"&&(e=e.slice(1)),e==="0")return!1;for(;e[++r]==="0";);return r>0},Vf=(t,e,r)=>typeof t=="string"||typeof e=="string"?!0:r.stringify===!0,Yf=(t,e,r)=>{if(e>0){let i=t[0]==="-"?"-":"";i&&(t=t.slice(1)),t=i+t.padStart(i?e-1:e,"0")}return r===!1?String(t):t},Gt=(t,e)=>{let r=t[0]==="-"?"-":"";for(r&&(t=t.slice(1),e--);t.length{t.negatives.sort((x,d)=>xd?1:0),t.positives.sort((x,d)=>xd?1:0);let i=e.capture?"":"?:",a="",c="",u;return t.positives.length&&(a=t.positives.map(x=>Gt(String(x),r)).join("|")),t.negatives.length&&(c=`-(${i}${t.negatives.map(x=>Gt(String(x),r)).join("|")})`),a&&c?u=`${a}|${c}`:u=a||c,e.wrap?`(${i}${u})`:u},Ya=(t,e,r,i)=>{if(r)return Va(t,e,{wrap:!1,...i});let a=String.fromCharCode(t);if(t===e)return a;let c=String.fromCharCode(e);return`[${a}-${c}]`},Qa=(t,e,r)=>{if(Array.isArray(t)){let i=r.wrap===!0,a=r.capture?"":"?:";return i?`(${a}${t.join("|")})`:t.join("|")}return Va(t,e,r)},Za=(...t)=>new RangeError("Invalid range arguments: "+Kf.inspect(...t)),eo=(t,e,r)=>{if(r.strictRanges===!0)throw Za([t,e]);return[]},Zf=(t,e)=>{if(e.strictRanges===!0)throw new TypeError(`Expected step "${t}" to be a number`);return[]},ep=(t,e,r=1,i={})=>{let a=Number(t),c=Number(e);if(!Number.isInteger(a)||!Number.isInteger(c)){if(i.strictRanges===!0)throw Za([t,e]);return[]}a===0&&(a=0),c===0&&(c=0);let u=a>c,x=String(t),d=String(e),p=String(r);r=Math.max(Math.abs(r),1);let h=pi(x)||pi(d)||pi(p),m=h?Math.max(x.length,d.length,p.length):0,l=h===!1&&Vf(t,e,i)===!1,n=i.transform||Wf(l);if(i.toRegex&&r===1)return Ya(Gt(t,m),Gt(e,m),!0,i);let s={negatives:[],positives:[]},o=g=>s[g<0?"negatives":"positives"].push(Math.abs(g)),f=[],E=0;for(;u?a>=c:a<=c;)i.toRegex===!0&&r>1?o(a):f.push(Yf(n(a,E),m,l)),a=u?a-r:a+r,E++;return i.toRegex===!0?r>1?Qf(s,i,m):Qa(f,null,{wrap:!1,...i}):f},tp=(t,e,r=1,i={})=>{if(!vt(t)&&t.length>1||!vt(e)&&e.length>1)return eo(t,e,i);let a=i.transform||(l=>String.fromCharCode(l)),c=`${t}`.charCodeAt(0),u=`${e}`.charCodeAt(0),x=c>u,d=Math.min(c,u),p=Math.max(c,u);if(i.toRegex&&r===1)return Ya(d,p,!1,i);let h=[],m=0;for(;x?c>=u:c<=u;)h.push(a(c,m)),c=x?c-r:c+r,m++;return i.toRegex===!0?Qa(h,null,{wrap:!1,options:i}):h},qt=(t,e,r,i={})=>{if(e==null&&fi(t))return[t];if(!fi(t)||!fi(e))return eo(t,e,i);if(typeof r=="function")return qt(t,e,1,{transform:r});if(Wa(r))return qt(t,e,0,r);let a={...i};return a.capture===!0&&(a.wrap=!0),r=r||a.step||1,vt(r)?vt(t)&&vt(e)?ep(t,e,r,a):tp(t,e,Math.max(Math.abs(r),1),a):r!=null&&!Wa(r)?Zf(r,a):qt(t,e,1,r)};to.exports=qt});var no=_((Sx,io)=>{"use strict";var rp=di(),ro=Jt(),ip=(t,e={})=>{let r=(i,a={})=>{let c=ro.isInvalidBrace(a),u=i.invalid===!0&&e.escapeInvalid===!0,x=c===!0||u===!0,d=e.escapeInvalid===!0?"\\":"",p="";if(i.isOpen===!0)return d+i.value;if(i.isClose===!0)return console.log("node.isClose",d,i.value),d+i.value;if(i.type==="open")return x?d+i.value:"(";if(i.type==="close")return x?d+i.value:")";if(i.type==="comma")return i.prev.type==="comma"?"":x?i.value:"|";if(i.value)return i.value;if(i.nodes&&i.ranges>0){let h=ro.reduce(i.nodes),m=rp(...h,{...e,wrap:!1,toRegex:!0,strictZeros:!0});if(m.length!==0)return h.length>1&&m.length>1?`(${m})`:m}if(i.nodes)for(let h of i.nodes)p+=r(h,i);return p};return r(t)};io.exports=ip});var oo=_((Ax,ao)=>{"use strict";var np=di(),so=zt(),nt=Jt(),qe=(t="",e="",r=!1)=>{let i=[];if(t=[].concat(t),e=[].concat(e),!e.length)return t;if(!t.length)return r?nt.flatten(e).map(a=>`{${a}}`):e;for(let a of t)if(Array.isArray(a))for(let c of a)i.push(qe(c,e,r));else for(let c of e)r===!0&&typeof c=="string"&&(c=`{${c}}`),i.push(Array.isArray(c)?qe(a,c,r):a+c);return nt.flatten(i)},sp=(t,e={})=>{let r=e.rangeLimit===void 0?1e3:e.rangeLimit,i=(a,c={})=>{a.queue=[];let u=c,x=c.queue;for(;u.type!=="brace"&&u.type!=="root"&&u.parent;)u=u.parent,x=u.queue;if(a.invalid||a.dollar){x.push(qe(x.pop(),so(a,e)));return}if(a.type==="brace"&&a.invalid!==!0&&a.nodes.length===2){x.push(qe(x.pop(),["{}"]));return}if(a.nodes&&a.ranges>0){let m=nt.reduce(a.nodes);if(nt.exceedsLimit(...m,e.step,r))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let l=np(...m,e);l.length===0&&(l=so(a,e)),x.push(qe(x.pop(),l)),a.nodes=[];return}let d=nt.encloseBrace(a),p=a.queue,h=a;for(;h.type!=="brace"&&h.type!=="root"&&h.parent;)h=h.parent,p=h.queue;for(let m=0;m{"use strict";uo.exports={MAX_LENGTH:1e4,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` +`,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var mo=_((bx,po)=>{"use strict";var ap=zt(),{MAX_LENGTH:lo,CHAR_BACKSLASH:mi,CHAR_BACKTICK:op,CHAR_COMMA:up,CHAR_DOT:cp,CHAR_LEFT_PARENTHESES:lp,CHAR_RIGHT_PARENTHESES:hp,CHAR_LEFT_CURLY_BRACE:fp,CHAR_RIGHT_CURLY_BRACE:pp,CHAR_LEFT_SQUARE_BRACKET:ho,CHAR_RIGHT_SQUARE_BRACKET:fo,CHAR_DOUBLE_QUOTE:dp,CHAR_SINGLE_QUOTE:mp,CHAR_NO_BREAK_SPACE:xp,CHAR_ZERO_WIDTH_NOBREAK_SPACE:Ep}=co(),gp=(t,e={})=>{if(typeof t!="string")throw new TypeError("Expected a string");let r=e||{},i=typeof r.maxLength=="number"?Math.min(lo,r.maxLength):lo;if(t.length>i)throw new SyntaxError(`Input length (${t.length}), exceeds max characters (${i})`);let a={type:"root",input:t,nodes:[]},c=[a],u=a,x=a,d=0,p=t.length,h=0,m=0,l,n=()=>t[h++],s=o=>{if(o.type==="text"&&x.type==="dot"&&(x.type="text"),x&&x.type==="text"&&o.type==="text"){x.value+=o.value;return}return u.nodes.push(o),o.parent=u,o.prev=x,x=o,o};for(s({type:"bos"});h0){if(u.ranges>0){u.ranges=0;let o=u.nodes.shift();u.nodes=[o,{type:"text",value:ap(u)}]}s({type:"comma",value:l}),u.commas++;continue}if(l===cp&&m>0&&u.commas===0){let o=u.nodes;if(m===0||o.length===0){s({type:"text",value:l});continue}if(x.type==="dot"){if(u.range=[],x.value+=l,x.type="range",u.nodes.length!==3&&u.nodes.length!==5){u.invalid=!0,u.ranges=0,x.type="text";continue}u.ranges++,u.args=[];continue}if(x.type==="range"){o.pop();let f=o[o.length-1];f.value+=x.value+l,x=f,u.ranges--;continue}s({type:"dot",value:l});continue}s({type:"text",value:l})}do if(u=c.pop(),u.type!=="root"){u.nodes.forEach(E=>{E.nodes||(E.type==="open"&&(E.isOpen=!0),E.type==="close"&&(E.isClose=!0),E.nodes||(E.type="text"),E.invalid=!0)});let o=c[c.length-1],f=o.nodes.indexOf(u);o.nodes.splice(f,1,...u.nodes)}while(c.length>0);return s({type:"eos"}),a};po.exports=gp});var go=_((Fx,Eo)=>{"use strict";var xo=zt(),Dp=no(),yp=oo(),vp=mo(),ce=(t,e={})=>{let r=[];if(Array.isArray(t))for(let i of t){let a=ce.create(i,e);Array.isArray(a)?r.push(...a):r.push(a)}else r=[].concat(ce.create(t,e));return e&&e.expand===!0&&e.nodupes===!0&&(r=[...new Set(r)]),r};ce.parse=(t,e={})=>vp(t,e);ce.stringify=(t,e={})=>xo(typeof t=="string"?ce.parse(t,e):t,e);ce.compile=(t,e={})=>(typeof t=="string"&&(t=ce.parse(t,e)),Dp(t,e));ce.expand=(t,e={})=>{typeof t=="string"&&(t=ce.parse(t,e));let r=yp(t,e);return e.noempty===!0&&(r=r.filter(Boolean)),e.nodupes===!0&&(r=[...new Set(r)]),r};ce.create=(t,e={})=>t===""||t.length<3?[t]:e.expand!==!0?ce.compile(t,e):ce.expand(t,e);Eo.exports=ce});var St=_((wx,Ao)=>{"use strict";var Sp=O("path"),Ae="\\\\/",Do=`[^${Ae}]`,ke="\\.",Ap="\\+",Cp="\\?",Kt="\\/",bp="(?=.)",yo="[^/]",xi=`(?:${Kt}|$)`,vo=`(?:^|${Kt})`,Ei=`${ke}{1,2}${xi}`,Fp=`(?!${ke})`,wp=`(?!${vo}${Ei})`,_p=`(?!${ke}{0,1}${xi})`,kp=`(?!${Ei})`,Tp=`[^.${Kt}]`,Bp=`${yo}*?`,So={DOT_LITERAL:ke,PLUS_LITERAL:Ap,QMARK_LITERAL:Cp,SLASH_LITERAL:Kt,ONE_CHAR:bp,QMARK:yo,END_ANCHOR:xi,DOTS_SLASH:Ei,NO_DOT:Fp,NO_DOTS:wp,NO_DOT_SLASH:_p,NO_DOTS_SLASH:kp,QMARK_NO_DOT:Tp,STAR:Bp,START_ANCHOR:vo},Pp={...So,SLASH_LITERAL:`[${Ae}]`,QMARK:Do,STAR:`${Do}*?`,DOTS_SLASH:`${ke}{1,2}(?:[${Ae}]|$)`,NO_DOT:`(?!${ke})`,NO_DOTS:`(?!(?:^|[${Ae}])${ke}{1,2}(?:[${Ae}]|$))`,NO_DOT_SLASH:`(?!${ke}{0,1}(?:[${Ae}]|$))`,NO_DOTS_SLASH:`(?!${ke}{1,2}(?:[${Ae}]|$))`,QMARK_NO_DOT:`[^.${Ae}]`,START_ANCHOR:`(?:^|[${Ae}])`,END_ANCHOR:`(?:[${Ae}]|$)`},Rp={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};Ao.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:Rp,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:Sp.sep,extglobChars(t){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${t.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(t){return t===!0?Pp:So}}});var At=_(se=>{"use strict";var Ip=O("path"),Np=process.platform==="win32",{REGEX_BACKSLASH:Lp,REGEX_REMOVE_BACKSLASH:Op,REGEX_SPECIAL_CHARS:Mp,REGEX_SPECIAL_CHARS_GLOBAL:Hp}=St();se.isObject=t=>t!==null&&typeof t=="object"&&!Array.isArray(t);se.hasRegexChars=t=>Mp.test(t);se.isRegexChar=t=>t.length===1&&se.hasRegexChars(t);se.escapeRegex=t=>t.replace(Hp,"\\$1");se.toPosixSlashes=t=>t.replace(Lp,"/");se.removeBackslashes=t=>t.replace(Op,e=>e==="\\"?"":e);se.supportsLookbehinds=()=>{let t=process.version.slice(1).split(".").map(Number);return t.length===3&&t[0]>=9||t[0]===8&&t[1]>=10};se.isWindows=t=>t&&typeof t.windows=="boolean"?t.windows:Np===!0||Ip.sep==="\\";se.escapeLast=(t,e,r)=>{let i=t.lastIndexOf(e,r);return i===-1?t:t[i-1]==="\\"?se.escapeLast(t,e,i-1):`${t.slice(0,i)}\\${t.slice(i)}`};se.removePrefix=(t,e={})=>{let r=t;return r.startsWith("./")&&(r=r.slice(2),e.prefix="./"),r};se.wrapOutput=(t,e={},r={})=>{let i=r.contains?"":"^",a=r.contains?"":"$",c=`${i}(?:${t})${a}`;return e.negated===!0&&(c=`(?:^(?!${c}).*$)`),c}});var Bo=_((kx,To)=>{"use strict";var Co=At(),{CHAR_ASTERISK:gi,CHAR_AT:Xp,CHAR_BACKWARD_SLASH:Ct,CHAR_COMMA:Up,CHAR_DOT:Di,CHAR_EXCLAMATION_MARK:yi,CHAR_FORWARD_SLASH:ko,CHAR_LEFT_CURLY_BRACE:vi,CHAR_LEFT_PARENTHESES:Si,CHAR_LEFT_SQUARE_BRACKET:$p,CHAR_PLUS:jp,CHAR_QUESTION_MARK:bo,CHAR_RIGHT_CURLY_BRACE:Jp,CHAR_RIGHT_PARENTHESES:Fo,CHAR_RIGHT_SQUARE_BRACKET:zp}=St(),wo=t=>t===ko||t===Ct,_o=t=>{t.isPrefix!==!0&&(t.depth=t.isGlobstar?1/0:1)},qp=(t,e)=>{let r=e||{},i=t.length-1,a=r.parts===!0||r.scanToEnd===!0,c=[],u=[],x=[],d=t,p=-1,h=0,m=0,l=!1,n=!1,s=!1,o=!1,f=!1,E=!1,g=!1,v=!1,F=!1,A=!1,T=0,k,P,N={value:"",depth:0,isGlob:!1},U=()=>p>=i,S=()=>d.charCodeAt(p+1),j=()=>(k=P,d.charCodeAt(++p));for(;p0&&(ye=d.slice(0,h),d=d.slice(h),m-=h),J&&s===!0&&m>0?(J=d.slice(0,m),C=d.slice(m)):s===!0?(J="",C=d):J=d,J&&J!==""&&J!=="/"&&J!==d&&wo(J.charCodeAt(J.length-1))&&(J=J.slice(0,-1)),r.unescape===!0&&(C&&(C=Co.removeBackslashes(C)),J&&g===!0&&(J=Co.removeBackslashes(J)));let b={prefix:ye,input:t,start:h,base:J,glob:C,isBrace:l,isBracket:n,isGlob:s,isExtglob:o,isGlobstar:f,negated:v,negatedExtglob:F};if(r.tokens===!0&&(b.maxDepth=0,wo(P)||u.push(N),b.tokens=u),r.parts===!0||r.tokens===!0){let Y;for(let $=0;${"use strict";var Wt=St(),le=At(),{MAX_LENGTH:Vt,POSIX_REGEX_SOURCE:Gp,REGEX_NON_SPECIAL_CHARS:Kp,REGEX_SPECIAL_CHARS_BACKREF:Wp,REPLACEMENTS:Po}=Wt,Vp=(t,e)=>{if(typeof e.expandRange=="function")return e.expandRange(...t,e);t.sort();let r=`[${t.join("-")}]`;try{new RegExp(r)}catch{return t.map(a=>le.escapeRegex(a)).join("..")}return r},st=(t,e)=>`Missing ${t}: "${e}" - use "\\\\${e}" to match literal characters`,Ai=(t,e)=>{if(typeof t!="string")throw new TypeError("Expected a string");t=Po[t]||t;let r={...e},i=typeof r.maxLength=="number"?Math.min(Vt,r.maxLength):Vt,a=t.length;if(a>i)throw new SyntaxError(`Input length: ${a}, exceeds maximum allowed length: ${i}`);let c={type:"bos",value:"",output:r.prepend||""},u=[c],x=r.capture?"":"?:",d=le.isWindows(e),p=Wt.globChars(d),h=Wt.extglobChars(p),{DOT_LITERAL:m,PLUS_LITERAL:l,SLASH_LITERAL:n,ONE_CHAR:s,DOTS_SLASH:o,NO_DOT:f,NO_DOT_SLASH:E,NO_DOTS_SLASH:g,QMARK:v,QMARK_NO_DOT:F,STAR:A,START_ANCHOR:T}=p,k=R=>`(${x}(?:(?!${T}${R.dot?o:m}).)*?)`,P=r.dot?"":f,N=r.dot?v:F,U=r.bash===!0?k(r):A;r.capture&&(U=`(${U})`),typeof r.noext=="boolean"&&(r.noextglob=r.noext);let S={input:t,index:-1,start:0,dot:r.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:u};t=le.removePrefix(t,S),a=t.length;let j=[],J=[],ye=[],C=c,b,Y=()=>S.index===a-1,$=S.peek=(R=1)=>t[S.index+R],he=S.advance=()=>t[++S.index]||"",fe=()=>t.slice(S.index+1),ie=(R="",z=0)=>{S.consumed+=R,S.index+=z},Ye=R=>{S.output+=R.output!=null?R.output:R.value,ie(R.value)},Ar=()=>{let R=1;for(;$()==="!"&&($(2)!=="("||$(3)==="?");)he(),S.start++,R++;return R%2===0?!1:(S.negated=!0,S.start++,!0)},Qe=R=>{S[R]++,ye.push(R)},we=R=>{S[R]--,ye.pop()},X=R=>{if(C.type==="globstar"){let z=S.braces>0&&(R.type==="comma"||R.type==="brace"),B=R.extglob===!0||j.length&&(R.type==="pipe"||R.type==="paren");R.type!=="slash"&&R.type!=="paren"&&!z&&!B&&(S.output=S.output.slice(0,-C.output.length),C.type="star",C.value="*",C.output=U,S.output+=C.output)}if(j.length&&R.type!=="paren"&&(j[j.length-1].inner+=R.value),(R.value||R.output)&&Ye(R),C&&C.type==="text"&&R.type==="text"){C.value+=R.value,C.output=(C.output||"")+R.value;return}R.prev=C,u.push(R),C=R},Ze=(R,z)=>{let B={...h[z],conditions:1,inner:""};B.prev=C,B.parens=S.parens,B.output=S.output;let H=(r.capture?"(":"")+B.open;Qe("parens"),X({type:R,value:z,output:S.output?"":s}),X({type:"paren",extglob:!0,value:he(),output:H}),j.push(B)},Cr=R=>{let z=R.close+(r.capture?")":""),B;if(R.type==="negate"){let H=U;if(R.inner&&R.inner.length>1&&R.inner.includes("/")&&(H=k(r)),(H!==U||Y()||/^\)+$/.test(fe()))&&(z=R.close=`)$))${H}`),R.inner.includes("*")&&(B=fe())&&/^\.[^\\/.]+$/.test(B)){let q=Ai(B,{...e,fastpaths:!1}).output;z=R.close=`)${q})${H})`}R.prev.type==="bos"&&(S.negatedExtglob=!0)}X({type:"paren",extglob:!0,value:b,output:z}),we("parens")};if(r.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(t)){let R=!1,z=t.replace(Wp,(B,H,q,Q,K,pt)=>Q==="\\"?(R=!0,B):Q==="?"?H?H+Q+(K?v.repeat(K.length):""):pt===0?N+(K?v.repeat(K.length):""):v.repeat(q.length):Q==="."?m.repeat(q.length):Q==="*"?H?H+Q+(K?U:""):U:H?B:`\\${B}`);return R===!0&&(r.unescape===!0?z=z.replace(/\\/g,""):z=z.replace(/\\+/g,B=>B.length%2===0?"\\\\":B?"\\":"")),z===t&&r.contains===!0?(S.output=t,S):(S.output=le.wrapOutput(z,S,e),S)}for(;!Y();){if(b=he(),b==="\0")continue;if(b==="\\"){let B=$();if(B==="/"&&r.bash!==!0||B==="."||B===";")continue;if(!B){b+="\\",X({type:"text",value:b});continue}let H=/^\\+/.exec(fe()),q=0;if(H&&H[0].length>2&&(q=H[0].length,S.index+=q,q%2!==0&&(b+="\\")),r.unescape===!0?b=he():b+=he(),S.brackets===0){X({type:"text",value:b});continue}}if(S.brackets>0&&(b!=="]"||C.value==="["||C.value==="[^")){if(r.posix!==!1&&b===":"){let B=C.value.slice(1);if(B.includes("[")&&(C.posix=!0,B.includes(":"))){let H=C.value.lastIndexOf("["),q=C.value.slice(0,H),Q=C.value.slice(H+2),K=Gp[Q];if(K){C.value=q+K,S.backtrack=!0,he(),!c.output&&u.indexOf(C)===1&&(c.output=s);continue}}}(b==="["&&$()!==":"||b==="-"&&$()==="]")&&(b=`\\${b}`),b==="]"&&(C.value==="["||C.value==="[^")&&(b=`\\${b}`),r.posix===!0&&b==="!"&&C.value==="["&&(b="^"),C.value+=b,Ye({value:b});continue}if(S.quotes===1&&b!=='"'){b=le.escapeRegex(b),C.value+=b,Ye({value:b});continue}if(b==='"'){S.quotes=S.quotes===1?0:1,r.keepQuotes===!0&&X({type:"text",value:b});continue}if(b==="("){Qe("parens"),X({type:"paren",value:b});continue}if(b===")"){if(S.parens===0&&r.strictBrackets===!0)throw new SyntaxError(st("opening","("));let B=j[j.length-1];if(B&&S.parens===B.parens+1){Cr(j.pop());continue}X({type:"paren",value:b,output:S.parens?")":"\\)"}),we("parens");continue}if(b==="["){if(r.nobracket===!0||!fe().includes("]")){if(r.nobracket!==!0&&r.strictBrackets===!0)throw new SyntaxError(st("closing","]"));b=`\\${b}`}else Qe("brackets");X({type:"bracket",value:b});continue}if(b==="]"){if(r.nobracket===!0||C&&C.type==="bracket"&&C.value.length===1){X({type:"text",value:b,output:`\\${b}`});continue}if(S.brackets===0){if(r.strictBrackets===!0)throw new SyntaxError(st("opening","["));X({type:"text",value:b,output:`\\${b}`});continue}we("brackets");let B=C.value.slice(1);if(C.posix!==!0&&B[0]==="^"&&!B.includes("/")&&(b=`/${b}`),C.value+=b,Ye({value:b}),r.literalBrackets===!1||le.hasRegexChars(B))continue;let H=le.escapeRegex(C.value);if(S.output=S.output.slice(0,-C.value.length),r.literalBrackets===!0){S.output+=H,C.value=H;continue}C.value=`(${x}${H}|${C.value})`,S.output+=C.value;continue}if(b==="{"&&r.nobrace!==!0){Qe("braces");let B={type:"brace",value:b,output:"(",outputIndex:S.output.length,tokensIndex:S.tokens.length};J.push(B),X(B);continue}if(b==="}"){let B=J[J.length-1];if(r.nobrace===!0||!B){X({type:"text",value:b,output:b});continue}let H=")";if(B.dots===!0){let q=u.slice(),Q=[];for(let K=q.length-1;K>=0&&(u.pop(),q[K].type!=="brace");K--)q[K].type!=="dots"&&Q.unshift(q[K].value);H=Vp(Q,r),S.backtrack=!0}if(B.comma!==!0&&B.dots!==!0){let q=S.output.slice(0,B.outputIndex),Q=S.tokens.slice(B.tokensIndex);B.value=B.output="\\{",b=H="\\}",S.output=q;for(let K of Q)S.output+=K.output||K.value}X({type:"brace",value:b,output:H}),we("braces"),J.pop();continue}if(b==="|"){j.length>0&&j[j.length-1].conditions++,X({type:"text",value:b});continue}if(b===","){let B=b,H=J[J.length-1];H&&ye[ye.length-1]==="braces"&&(H.comma=!0,B="|"),X({type:"comma",value:b,output:B});continue}if(b==="/"){if(C.type==="dot"&&S.index===S.start+1){S.start=S.index+1,S.consumed="",S.output="",u.pop(),C=c;continue}X({type:"slash",value:b,output:n});continue}if(b==="."){if(S.braces>0&&C.type==="dot"){C.value==="."&&(C.output=m);let B=J[J.length-1];C.type="dots",C.output+=b,C.value+=b,B.dots=!0;continue}if(S.braces+S.parens===0&&C.type!=="bos"&&C.type!=="slash"){X({type:"text",value:b,output:m});continue}X({type:"dot",value:b,output:m});continue}if(b==="?"){if(!(C&&C.value==="(")&&r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Ze("qmark",b);continue}if(C&&C.type==="paren"){let H=$(),q=b;if(H==="<"&&!le.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(C.value==="("&&!/[!=<:]/.test(H)||H==="<"&&!/<([!=]|\w+>)/.test(fe()))&&(q=`\\${b}`),X({type:"text",value:b,output:q});continue}if(r.dot!==!0&&(C.type==="slash"||C.type==="bos")){X({type:"qmark",value:b,output:F});continue}X({type:"qmark",value:b,output:v});continue}if(b==="!"){if(r.noextglob!==!0&&$()==="("&&($(2)!=="?"||!/[!=<:]/.test($(3)))){Ze("negate",b);continue}if(r.nonegate!==!0&&S.index===0){Ar();continue}}if(b==="+"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Ze("plus",b);continue}if(C&&C.value==="("||r.regex===!1){X({type:"plus",value:b,output:l});continue}if(C&&(C.type==="bracket"||C.type==="paren"||C.type==="brace")||S.parens>0){X({type:"plus",value:b});continue}X({type:"plus",value:l});continue}if(b==="@"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){X({type:"at",extglob:!0,value:b,output:""});continue}X({type:"text",value:b});continue}if(b!=="*"){(b==="$"||b==="^")&&(b=`\\${b}`);let B=Kp.exec(fe());B&&(b+=B[0],S.index+=B[0].length),X({type:"text",value:b});continue}if(C&&(C.type==="globstar"||C.star===!0)){C.type="star",C.star=!0,C.value+=b,C.output=U,S.backtrack=!0,S.globstar=!0,ie(b);continue}let R=fe();if(r.noextglob!==!0&&/^\([^?]/.test(R)){Ze("star",b);continue}if(C.type==="star"){if(r.noglobstar===!0){ie(b);continue}let B=C.prev,H=B.prev,q=B.type==="slash"||B.type==="bos",Q=H&&(H.type==="star"||H.type==="globstar");if(r.bash===!0&&(!q||R[0]&&R[0]!=="/")){X({type:"star",value:b,output:""});continue}let K=S.braces>0&&(B.type==="comma"||B.type==="brace"),pt=j.length&&(B.type==="pipe"||B.type==="paren");if(!q&&B.type!=="paren"&&!K&&!pt){X({type:"star",value:b,output:""});continue}for(;R.slice(0,3)==="/**";){let et=t[S.index+4];if(et&&et!=="/")break;R=R.slice(3),ie("/**",3)}if(B.type==="bos"&&Y()){C.type="globstar",C.value+=b,C.output=k(r),S.output=C.output,S.globstar=!0,ie(b);continue}if(B.type==="slash"&&B.prev.type!=="bos"&&!Q&&Y()){S.output=S.output.slice(0,-(B.output+C.output).length),B.output=`(?:${B.output}`,C.type="globstar",C.output=k(r)+(r.strictSlashes?")":"|$)"),C.value+=b,S.globstar=!0,S.output+=B.output+C.output,ie(b);continue}if(B.type==="slash"&&B.prev.type!=="bos"&&R[0]==="/"){let et=R[1]!==void 0?"|$":"";S.output=S.output.slice(0,-(B.output+C.output).length),B.output=`(?:${B.output}`,C.type="globstar",C.output=`${k(r)}${n}|${n}${et})`,C.value+=b,S.output+=B.output+C.output,S.globstar=!0,ie(b+he()),X({type:"slash",value:"/",output:""});continue}if(B.type==="bos"&&R[0]==="/"){C.type="globstar",C.value+=b,C.output=`(?:^|${n}|${k(r)}${n})`,S.output=C.output,S.globstar=!0,ie(b+he()),X({type:"slash",value:"/",output:""});continue}S.output=S.output.slice(0,-C.output.length),C.type="globstar",C.output=k(r),C.value+=b,S.output+=C.output,S.globstar=!0,ie(b);continue}let z={type:"star",value:b,output:U};if(r.bash===!0){z.output=".*?",(C.type==="bos"||C.type==="slash")&&(z.output=P+z.output),X(z);continue}if(C&&(C.type==="bracket"||C.type==="paren")&&r.regex===!0){z.output=b,X(z);continue}(S.index===S.start||C.type==="slash"||C.type==="dot")&&(C.type==="dot"?(S.output+=E,C.output+=E):r.dot===!0?(S.output+=g,C.output+=g):(S.output+=P,C.output+=P),$()!=="*"&&(S.output+=s,C.output+=s)),X(z)}for(;S.brackets>0;){if(r.strictBrackets===!0)throw new SyntaxError(st("closing","]"));S.output=le.escapeLast(S.output,"["),we("brackets")}for(;S.parens>0;){if(r.strictBrackets===!0)throw new SyntaxError(st("closing",")"));S.output=le.escapeLast(S.output,"("),we("parens")}for(;S.braces>0;){if(r.strictBrackets===!0)throw new SyntaxError(st("closing","}"));S.output=le.escapeLast(S.output,"{"),we("braces")}if(r.strictSlashes!==!0&&(C.type==="star"||C.type==="bracket")&&X({type:"maybe_slash",value:"",output:`${n}?`}),S.backtrack===!0){S.output="";for(let R of S.tokens)S.output+=R.output!=null?R.output:R.value,R.suffix&&(S.output+=R.suffix)}return S};Ai.fastpaths=(t,e)=>{let r={...e},i=typeof r.maxLength=="number"?Math.min(Vt,r.maxLength):Vt,a=t.length;if(a>i)throw new SyntaxError(`Input length: ${a}, exceeds maximum allowed length: ${i}`);t=Po[t]||t;let c=le.isWindows(e),{DOT_LITERAL:u,SLASH_LITERAL:x,ONE_CHAR:d,DOTS_SLASH:p,NO_DOT:h,NO_DOTS:m,NO_DOTS_SLASH:l,STAR:n,START_ANCHOR:s}=Wt.globChars(c),o=r.dot?m:h,f=r.dot?l:h,E=r.capture?"":"?:",g={negated:!1,prefix:""},v=r.bash===!0?".*?":n;r.capture&&(v=`(${v})`);let F=P=>P.noglobstar===!0?v:`(${E}(?:(?!${s}${P.dot?p:u}).)*?)`,A=P=>{switch(P){case"*":return`${o}${d}${v}`;case".*":return`${u}${d}${v}`;case"*.*":return`${o}${v}${u}${d}${v}`;case"*/*":return`${o}${v}${x}${d}${f}${v}`;case"**":return o+F(r);case"**/*":return`(?:${o}${F(r)}${x})?${f}${d}${v}`;case"**/*.*":return`(?:${o}${F(r)}${x})?${f}${v}${u}${d}${v}`;case"**/.*":return`(?:${o}${F(r)}${x})?${u}${d}${v}`;default:{let N=/^(.*?)\.(\w+)$/.exec(P);if(!N)return;let U=A(N[1]);return U?U+u+N[2]:void 0}}},T=le.removePrefix(t,g),k=A(T);return k&&r.strictSlashes!==!0&&(k+=`${x}?`),k};Ro.exports=Ai});var Lo=_((Bx,No)=>{"use strict";var Yp=O("path"),Qp=Bo(),Ci=Io(),bi=At(),Zp=St(),ed=t=>t&&typeof t=="object"&&!Array.isArray(t),W=(t,e,r=!1)=>{if(Array.isArray(t)){let h=t.map(l=>W(l,e,r));return l=>{for(let n of h){let s=n(l);if(s)return s}return!1}}let i=ed(t)&&t.tokens&&t.input;if(t===""||typeof t!="string"&&!i)throw new TypeError("Expected pattern to be a non-empty string");let a=e||{},c=bi.isWindows(e),u=i?W.compileRe(t,e):W.makeRe(t,e,!1,!0),x=u.state;delete u.state;let d=()=>!1;if(a.ignore){let h={...e,ignore:null,onMatch:null,onResult:null};d=W(a.ignore,h,r)}let p=(h,m=!1)=>{let{isMatch:l,match:n,output:s}=W.test(h,u,e,{glob:t,posix:c}),o={glob:t,state:x,regex:u,posix:c,input:h,output:s,match:n,isMatch:l};return typeof a.onResult=="function"&&a.onResult(o),l===!1?(o.isMatch=!1,m?o:!1):d(h)?(typeof a.onIgnore=="function"&&a.onIgnore(o),o.isMatch=!1,m?o:!1):(typeof a.onMatch=="function"&&a.onMatch(o),m?o:!0)};return r&&(p.state=x),p};W.test=(t,e,r,{glob:i,posix:a}={})=>{if(typeof t!="string")throw new TypeError("Expected input to be a string");if(t==="")return{isMatch:!1,output:""};let c=r||{},u=c.format||(a?bi.toPosixSlashes:null),x=t===i,d=x&&u?u(t):t;return x===!1&&(d=u?u(t):t,x=d===i),(x===!1||c.capture===!0)&&(c.matchBase===!0||c.basename===!0?x=W.matchBase(t,e,r,a):x=e.exec(d)),{isMatch:!!x,match:x,output:d}};W.matchBase=(t,e,r,i=bi.isWindows(r))=>(e instanceof RegExp?e:W.makeRe(e,r)).test(Yp.basename(t));W.isMatch=(t,e,r)=>W(e,r)(t);W.parse=(t,e)=>Array.isArray(t)?t.map(r=>W.parse(r,e)):Ci(t,{...e,fastpaths:!1});W.scan=(t,e)=>Qp(t,e);W.compileRe=(t,e,r=!1,i=!1)=>{if(r===!0)return t.output;let a=e||{},c=a.contains?"":"^",u=a.contains?"":"$",x=`${c}(?:${t.output})${u}`;t&&t.negated===!0&&(x=`^(?!${x}).*$`);let d=W.toRegex(x,e);return i===!0&&(d.state=t),d};W.makeRe=(t,e={},r=!1,i=!1)=>{if(!t||typeof t!="string")throw new TypeError("Expected a non-empty string");let a={negated:!1,fastpaths:!0};return e.fastpaths!==!1&&(t[0]==="."||t[0]==="*")&&(a.output=Ci.fastpaths(t,e)),a.output||(a=Ci(t,e)),W.compileRe(a,e,r,i)};W.toRegex=(t,e)=>{try{let r=e||{};return new RegExp(t,r.flags||(r.nocase?"i":""))}catch(r){if(e&&e.debug===!0)throw r;return/$^/}};W.constants=Zp;No.exports=W});var Mo=_((Px,Oo)=>{"use strict";Oo.exports=Lo()});var Jo=_((Rx,jo)=>{"use strict";var Xo=O("util"),Uo=go(),Ce=Mo(),Fi=At(),Ho=t=>t===""||t==="./",$o=t=>{let e=t.indexOf("{");return e>-1&&t.indexOf("}",e)>-1},G=(t,e,r)=>{e=[].concat(e),t=[].concat(t);let i=new Set,a=new Set,c=new Set,u=0,x=h=>{c.add(h.output),r&&r.onResult&&r.onResult(h)};for(let h=0;h!i.has(h));if(r&&p.length===0){if(r.failglob===!0)throw new Error(`No matches found for "${e.join(", ")}"`);if(r.nonull===!0||r.nullglob===!0)return r.unescape?e.map(h=>h.replace(/\\/g,"")):e}return p};G.match=G;G.matcher=(t,e)=>Ce(t,e);G.isMatch=(t,e,r)=>Ce(e,r)(t);G.any=G.isMatch;G.not=(t,e,r={})=>{e=[].concat(e).map(String);let i=new Set,a=[],c=x=>{r.onResult&&r.onResult(x),a.push(x.output)},u=new Set(G(t,e,{...r,onResult:c}));for(let x of a)u.has(x)||i.add(x);return[...i]};G.contains=(t,e,r)=>{if(typeof t!="string")throw new TypeError(`Expected a string: "${Xo.inspect(t)}"`);if(Array.isArray(e))return e.some(i=>G.contains(t,i,r));if(typeof e=="string"){if(Ho(t)||Ho(e))return!1;if(t.includes(e)||t.startsWith("./")&&t.slice(2).includes(e))return!0}return G.isMatch(t,e,{...r,contains:!0})};G.matchKeys=(t,e,r)=>{if(!Fi.isObject(t))throw new TypeError("Expected the first argument to be an object");let i=G(Object.keys(t),e,r),a={};for(let c of i)a[c]=t[c];return a};G.some=(t,e,r)=>{let i=[].concat(t);for(let a of[].concat(e)){let c=Ce(String(a),r);if(i.some(u=>c(u)))return!0}return!1};G.every=(t,e,r)=>{let i=[].concat(t);for(let a of[].concat(e)){let c=Ce(String(a),r);if(!i.every(u=>c(u)))return!1}return!0};G.all=(t,e,r)=>{if(typeof t!="string")throw new TypeError(`Expected a string: "${Xo.inspect(t)}"`);return[].concat(e).every(i=>Ce(i,r)(t))};G.capture=(t,e,r)=>{let i=Fi.isWindows(r),c=Ce.makeRe(String(t),{...r,capture:!0}).exec(i?Fi.toPosixSlashes(e):e);if(c)return c.slice(1).map(u=>u===void 0?"":u)};G.makeRe=(...t)=>Ce.makeRe(...t);G.scan=(...t)=>Ce.scan(...t);G.parse=(t,e)=>{let r=[];for(let i of[].concat(t||[]))for(let a of Uo(String(i),e))r.push(Ce.parse(a,e));return r};G.braces=(t,e)=>{if(typeof t!="string")throw new TypeError("Expected a string");return e&&e.nobrace===!0||!$o(t)?[t]:Uo(t,e)};G.braceExpand=(t,e)=>{if(typeof t!="string")throw new TypeError("Expected a string");return G.braces(t,{...e,expand:!0})};G.hasBraces=$o;jo.exports=G});var Qo=_(L=>{"use strict";Object.defineProperty(L,"__esModule",{value:!0});L.removeDuplicateSlashes=L.matchAny=L.convertPatternsToRe=L.makeRe=L.getPatternParts=L.expandBraceExpansion=L.expandPatternsWithBraceExpansion=L.isAffectDepthOfReadingPattern=L.endsWithSlashGlobStar=L.hasGlobStar=L.getBaseDirectory=L.isPatternRelatedToParentDirectory=L.getPatternsOutsideCurrentDirectory=L.getPatternsInsideCurrentDirectory=L.getPositivePatterns=L.getNegativePatterns=L.isPositivePattern=L.isNegativePattern=L.convertToNegativePattern=L.convertToPositivePattern=L.isDynamicPattern=L.isStaticPattern=void 0;var td=O("path"),rd=Na(),wi=Jo(),zo="**",id="\\",nd=/[*?]|^!/,sd=/\[[^[]*]/,ad=/(?:^|[^!*+?@])\([^(]*\|[^|]*\)/,od=/[!*+?@]\([^(]*\)/,ud=/,|\.\./,cd=/(?!^)\/{2,}/g;function qo(t,e={}){return!Go(t,e)}L.isStaticPattern=qo;function Go(t,e={}){return t===""?!1:!!(e.caseSensitiveMatch===!1||t.includes(id)||nd.test(t)||sd.test(t)||ad.test(t)||e.extglob!==!1&&od.test(t)||e.braceExpansion!==!1&&ld(t))}L.isDynamicPattern=Go;function ld(t){let e=t.indexOf("{");if(e===-1)return!1;let r=t.indexOf("}",e+1);if(r===-1)return!1;let i=t.slice(e,r);return ud.test(i)}function hd(t){return Yt(t)?t.slice(1):t}L.convertToPositivePattern=hd;function fd(t){return"!"+t}L.convertToNegativePattern=fd;function Yt(t){return t.startsWith("!")&&t[1]!=="("}L.isNegativePattern=Yt;function Ko(t){return!Yt(t)}L.isPositivePattern=Ko;function pd(t){return t.filter(Yt)}L.getNegativePatterns=pd;function dd(t){return t.filter(Ko)}L.getPositivePatterns=dd;function md(t){return t.filter(e=>!_i(e))}L.getPatternsInsideCurrentDirectory=md;function xd(t){return t.filter(_i)}L.getPatternsOutsideCurrentDirectory=xd;function _i(t){return t.startsWith("..")||t.startsWith("./..")}L.isPatternRelatedToParentDirectory=_i;function Ed(t){return rd(t,{flipBackslashes:!1})}L.getBaseDirectory=Ed;function gd(t){return t.includes(zo)}L.hasGlobStar=gd;function Wo(t){return t.endsWith("/"+zo)}L.endsWithSlashGlobStar=Wo;function Dd(t){let e=td.basename(t);return Wo(t)||qo(e)}L.isAffectDepthOfReadingPattern=Dd;function yd(t){return t.reduce((e,r)=>e.concat(Vo(r)),[])}L.expandPatternsWithBraceExpansion=yd;function Vo(t){let e=wi.braces(t,{expand:!0,nodupes:!0,keepEscaping:!0});return e.sort((r,i)=>r.length-i.length),e.filter(r=>r!=="")}L.expandBraceExpansion=Vo;function vd(t,e){let{parts:r}=wi.scan(t,Object.assign(Object.assign({},e),{parts:!0}));return r.length===0&&(r=[t]),r[0].startsWith("/")&&(r[0]=r[0].slice(1),r.unshift("")),r}L.getPatternParts=vd;function Yo(t,e){return wi.makeRe(t,e)}L.makeRe=Yo;function Sd(t,e){return t.map(r=>Yo(r,e))}L.convertPatternsToRe=Sd;function Ad(t,e){return e.some(r=>r.test(t))}L.matchAny=Ad;function Cd(t){return t.replace(cd,"/")}L.removeDuplicateSlashes=Cd});var ru=_((Nx,tu)=>{"use strict";var bd=O("stream"),Zo=bd.PassThrough,Fd=Array.prototype.slice;tu.exports=wd;function wd(){let t=[],e=Fd.call(arguments),r=!1,i=e[e.length-1];i&&!Array.isArray(i)&&i.pipe==null?e.pop():i={};let a=i.end!==!1,c=i.pipeError===!0;i.objectMode==null&&(i.objectMode=!0),i.highWaterMark==null&&(i.highWaterMark=64*1024);let u=Zo(i);function x(){for(let h=0,m=arguments.length;h0||(r=!1,d())}function n(s){function o(){s.removeListener("merge2UnpipeEnd",o),s.removeListener("end",o),c&&s.removeListener("error",f),l()}function f(E){u.emit("error",E)}if(s._readableState.endEmitted)return l();s.on("merge2UnpipeEnd",o),s.on("end",o),c&&s.on("error",f),s.pipe(u,{end:!1}),s.resume()}for(let s=0;s{"use strict";Object.defineProperty(Qt,"__esModule",{value:!0});Qt.merge=void 0;var _d=ru();function kd(t){let e=_d(t);return t.forEach(r=>{r.once("error",i=>e.emit("error",i))}),e.once("close",()=>iu(t)),e.once("end",()=>iu(t)),e}Qt.merge=kd;function iu(t){t.forEach(e=>e.emit("close"))}});var su=_(at=>{"use strict";Object.defineProperty(at,"__esModule",{value:!0});at.isEmpty=at.isString=void 0;function Td(t){return typeof t=="string"}at.isString=Td;function Bd(t){return t===""}at.isEmpty=Bd});var Te=_(te=>{"use strict";Object.defineProperty(te,"__esModule",{value:!0});te.string=te.stream=te.pattern=te.path=te.fs=te.errno=te.array=void 0;var Pd=Sa();te.array=Pd;var Rd=Aa();te.errno=Rd;var Id=Ca();te.fs=Id;var Nd=_a();te.path=Nd;var Ld=Qo();te.pattern=Ld;var Od=nu();te.stream=Od;var Md=su();te.string=Md});var cu=_(re=>{"use strict";Object.defineProperty(re,"__esModule",{value:!0});re.convertPatternGroupToTask=re.convertPatternGroupsToTasks=re.groupPatternsByBaseDirectory=re.getNegativePatternsAsPositive=re.getPositivePatterns=re.convertPatternsToTasks=re.generate=void 0;var ge=Te();function Hd(t,e){let r=au(t,e),i=au(e.ignore,e),a=ou(r),c=uu(r,i),u=a.filter(h=>ge.pattern.isStaticPattern(h,e)),x=a.filter(h=>ge.pattern.isDynamicPattern(h,e)),d=ki(u,c,!1),p=ki(x,c,!0);return d.concat(p)}re.generate=Hd;function au(t,e){let r=t;return e.braceExpansion&&(r=ge.pattern.expandPatternsWithBraceExpansion(r)),e.baseNameMatch&&(r=r.map(i=>i.includes("/")?i:`**/${i}`)),r.map(i=>ge.pattern.removeDuplicateSlashes(i))}function ki(t,e,r){let i=[],a=ge.pattern.getPatternsOutsideCurrentDirectory(t),c=ge.pattern.getPatternsInsideCurrentDirectory(t),u=Ti(a),x=Ti(c);return i.push(...Bi(u,e,r)),"."in x?i.push(Pi(".",c,e,r)):i.push(...Bi(x,e,r)),i}re.convertPatternsToTasks=ki;function ou(t){return ge.pattern.getPositivePatterns(t)}re.getPositivePatterns=ou;function uu(t,e){return ge.pattern.getNegativePatterns(t).concat(e).map(ge.pattern.convertToPositivePattern)}re.getNegativePatternsAsPositive=uu;function Ti(t){let e={};return t.reduce((r,i)=>{let a=ge.pattern.getBaseDirectory(i);return a in r?r[a].push(i):r[a]=[i],r},e)}re.groupPatternsByBaseDirectory=Ti;function Bi(t,e,r){return Object.keys(t).map(i=>Pi(i,t[i],e,r))}re.convertPatternGroupsToTasks=Bi;function Pi(t,e,r,i){return{dynamic:i,positive:e,negative:r,base:t,patterns:[].concat(e,r.map(ge.pattern.convertToNegativePattern))}}re.convertPatternGroupToTask=Pi});var hu=_(Zt=>{"use strict";Object.defineProperty(Zt,"__esModule",{value:!0});Zt.read=void 0;function Xd(t,e,r){e.fs.lstat(t,(i,a)=>{if(i!==null){lu(r,i);return}if(!a.isSymbolicLink()||!e.followSymbolicLink){Ri(r,a);return}e.fs.stat(t,(c,u)=>{if(c!==null){if(e.throwErrorOnBrokenSymbolicLink){lu(r,c);return}Ri(r,a);return}e.markSymbolicLink&&(u.isSymbolicLink=()=>!0),Ri(r,u)})})}Zt.read=Xd;function lu(t,e){t(e)}function Ri(t,e){t(null,e)}});var fu=_(er=>{"use strict";Object.defineProperty(er,"__esModule",{value:!0});er.read=void 0;function Ud(t,e){let r=e.fs.lstatSync(t);if(!r.isSymbolicLink()||!e.followSymbolicLink)return r;try{let i=e.fs.statSync(t);return e.markSymbolicLink&&(i.isSymbolicLink=()=>!0),i}catch(i){if(!e.throwErrorOnBrokenSymbolicLink)return r;throw i}}er.read=Ud});var pu=_(Ne=>{"use strict";Object.defineProperty(Ne,"__esModule",{value:!0});Ne.createFileSystemAdapter=Ne.FILE_SYSTEM_ADAPTER=void 0;var tr=O("fs");Ne.FILE_SYSTEM_ADAPTER={lstat:tr.lstat,stat:tr.stat,lstatSync:tr.lstatSync,statSync:tr.statSync};function $d(t){return t===void 0?Ne.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},Ne.FILE_SYSTEM_ADAPTER),t)}Ne.createFileSystemAdapter=$d});var du=_(Ni=>{"use strict";Object.defineProperty(Ni,"__esModule",{value:!0});var jd=pu(),Ii=class{constructor(e={}){this._options=e,this.followSymbolicLink=this._getValue(this._options.followSymbolicLink,!0),this.fs=jd.createFileSystemAdapter(this._options.fs),this.markSymbolicLink=this._getValue(this._options.markSymbolicLink,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0)}_getValue(e,r){return e??r}};Ni.default=Ii});var Ge=_(Le=>{"use strict";Object.defineProperty(Le,"__esModule",{value:!0});Le.statSync=Le.stat=Le.Settings=void 0;var mu=hu(),Jd=fu(),Li=du();Le.Settings=Li.default;function zd(t,e,r){if(typeof e=="function"){mu.read(t,Oi(),e);return}mu.read(t,Oi(e),r)}Le.stat=zd;function qd(t,e){let r=Oi(e);return Jd.read(t,r)}Le.statSync=qd;function Oi(t={}){return t instanceof Li.default?t:new Li.default(t)}});var gu=_((zx,Eu)=>{var xu;Eu.exports=typeof queueMicrotask=="function"?queueMicrotask.bind(typeof window<"u"?window:global):t=>(xu||(xu=Promise.resolve())).then(t).catch(e=>setTimeout(()=>{throw e},0))});var yu=_((qx,Du)=>{Du.exports=Kd;var Gd=gu();function Kd(t,e){let r,i,a,c=!0;Array.isArray(t)?(r=[],i=t.length):(a=Object.keys(t),r={},i=a.length);function u(d){function p(){e&&e(d,r),e=null}c?Gd(p):p()}function x(d,p,h){r[d]=h,(--i===0||p)&&u(p)}i?a?a.forEach(function(d){t[d](function(p,h){x(d,p,h)})}):t.forEach(function(d,p){d(function(h,m){x(p,h,m)})}):u(null),c=!1}});var Mi=_(ir=>{"use strict";Object.defineProperty(ir,"__esModule",{value:!0});ir.IS_SUPPORT_READDIR_WITH_FILE_TYPES=void 0;var rr=process.versions.node.split(".");if(rr[0]===void 0||rr[1]===void 0)throw new Error(`Unexpected behavior. The 'process.versions.node' variable has invalid value: ${process.versions.node}`);var vu=Number.parseInt(rr[0],10),Wd=Number.parseInt(rr[1],10),Su=10,Vd=10,Yd=vu>Su,Qd=vu===Su&&Wd>=Vd;ir.IS_SUPPORT_READDIR_WITH_FILE_TYPES=Yd||Qd});var Au=_(nr=>{"use strict";Object.defineProperty(nr,"__esModule",{value:!0});nr.createDirentFromStats=void 0;var Hi=class{constructor(e,r){this.name=e,this.isBlockDevice=r.isBlockDevice.bind(r),this.isCharacterDevice=r.isCharacterDevice.bind(r),this.isDirectory=r.isDirectory.bind(r),this.isFIFO=r.isFIFO.bind(r),this.isFile=r.isFile.bind(r),this.isSocket=r.isSocket.bind(r),this.isSymbolicLink=r.isSymbolicLink.bind(r)}};function Zd(t,e){return new Hi(t,e)}nr.createDirentFromStats=Zd});var Xi=_(sr=>{"use strict";Object.defineProperty(sr,"__esModule",{value:!0});sr.fs=void 0;var em=Au();sr.fs=em});var Ui=_(ar=>{"use strict";Object.defineProperty(ar,"__esModule",{value:!0});ar.joinPathSegments=void 0;function tm(t,e,r){return t.endsWith(r)?t+e:t+r+e}ar.joinPathSegments=tm});var ku=_(Oe=>{"use strict";Object.defineProperty(Oe,"__esModule",{value:!0});Oe.readdir=Oe.readdirWithFileTypes=Oe.read=void 0;var rm=Ge(),Cu=yu(),im=Mi(),bu=Xi(),Fu=Ui();function nm(t,e,r){if(!e.stats&&im.IS_SUPPORT_READDIR_WITH_FILE_TYPES){wu(t,e,r);return}_u(t,e,r)}Oe.read=nm;function wu(t,e,r){e.fs.readdir(t,{withFileTypes:!0},(i,a)=>{if(i!==null){or(r,i);return}let c=a.map(x=>({dirent:x,name:x.name,path:Fu.joinPathSegments(t,x.name,e.pathSegmentSeparator)}));if(!e.followSymbolicLinks){$i(r,c);return}let u=c.map(x=>sm(x,e));Cu(u,(x,d)=>{if(x!==null){or(r,x);return}$i(r,d)})})}Oe.readdirWithFileTypes=wu;function sm(t,e){return r=>{if(!t.dirent.isSymbolicLink()){r(null,t);return}e.fs.stat(t.path,(i,a)=>{if(i!==null){if(e.throwErrorOnBrokenSymbolicLink){r(i);return}r(null,t);return}t.dirent=bu.fs.createDirentFromStats(t.name,a),r(null,t)})}}function _u(t,e,r){e.fs.readdir(t,(i,a)=>{if(i!==null){or(r,i);return}let c=a.map(u=>{let x=Fu.joinPathSegments(t,u,e.pathSegmentSeparator);return d=>{rm.stat(x,e.fsStatSettings,(p,h)=>{if(p!==null){d(p);return}let m={name:u,path:x,dirent:bu.fs.createDirentFromStats(u,h)};e.stats&&(m.stats=h),d(null,m)})}});Cu(c,(u,x)=>{if(u!==null){or(r,u);return}$i(r,x)})})}Oe.readdir=_u;function or(t,e){t(e)}function $i(t,e){t(null,e)}});var Iu=_(Me=>{"use strict";Object.defineProperty(Me,"__esModule",{value:!0});Me.readdir=Me.readdirWithFileTypes=Me.read=void 0;var am=Ge(),om=Mi(),Tu=Xi(),Bu=Ui();function um(t,e){return!e.stats&&om.IS_SUPPORT_READDIR_WITH_FILE_TYPES?Pu(t,e):Ru(t,e)}Me.read=um;function Pu(t,e){return e.fs.readdirSync(t,{withFileTypes:!0}).map(i=>{let a={dirent:i,name:i.name,path:Bu.joinPathSegments(t,i.name,e.pathSegmentSeparator)};if(a.dirent.isSymbolicLink()&&e.followSymbolicLinks)try{let c=e.fs.statSync(a.path);a.dirent=Tu.fs.createDirentFromStats(a.name,c)}catch(c){if(e.throwErrorOnBrokenSymbolicLink)throw c}return a})}Me.readdirWithFileTypes=Pu;function Ru(t,e){return e.fs.readdirSync(t).map(i=>{let a=Bu.joinPathSegments(t,i,e.pathSegmentSeparator),c=am.statSync(a,e.fsStatSettings),u={name:i,path:a,dirent:Tu.fs.createDirentFromStats(i,c)};return e.stats&&(u.stats=c),u})}Me.readdir=Ru});var Nu=_(He=>{"use strict";Object.defineProperty(He,"__esModule",{value:!0});He.createFileSystemAdapter=He.FILE_SYSTEM_ADAPTER=void 0;var ot=O("fs");He.FILE_SYSTEM_ADAPTER={lstat:ot.lstat,stat:ot.stat,lstatSync:ot.lstatSync,statSync:ot.statSync,readdir:ot.readdir,readdirSync:ot.readdirSync};function cm(t){return t===void 0?He.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},He.FILE_SYSTEM_ADAPTER),t)}He.createFileSystemAdapter=cm});var Lu=_(Ji=>{"use strict";Object.defineProperty(Ji,"__esModule",{value:!0});var lm=O("path"),hm=Ge(),fm=Nu(),ji=class{constructor(e={}){this._options=e,this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!1),this.fs=fm.createFileSystemAdapter(this._options.fs),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,lm.sep),this.stats=this._getValue(this._options.stats,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0),this.fsStatSettings=new hm.Settings({followSymbolicLink:this.followSymbolicLinks,fs:this.fs,throwErrorOnBrokenSymbolicLink:this.throwErrorOnBrokenSymbolicLink})}_getValue(e,r){return e??r}};Ji.default=ji});var ur=_(Xe=>{"use strict";Object.defineProperty(Xe,"__esModule",{value:!0});Xe.Settings=Xe.scandirSync=Xe.scandir=void 0;var Ou=ku(),pm=Iu(),zi=Lu();Xe.Settings=zi.default;function dm(t,e,r){if(typeof e=="function"){Ou.read(t,qi(),e);return}Ou.read(t,qi(e),r)}Xe.scandir=dm;function mm(t,e){let r=qi(e);return pm.read(t,r)}Xe.scandirSync=mm;function qi(t={}){return t instanceof zi.default?t:new zi.default(t)}});var Hu=_((rE,Mu)=>{"use strict";function xm(t){var e=new t,r=e;function i(){var c=e;return c.next?e=c.next:(e=new t,r=e),c.next=null,c}function a(c){r.next=c,r=c}return{get:i,release:a}}Mu.exports=xm});var Uu=_((iE,Gi)=>{"use strict";var Em=Hu();function Xu(t,e,r){if(typeof t=="function"&&(r=e,e=t,t=null),!(r>=1))throw new Error("fastqueue concurrency must be equal to or greater than 1");var i=Em(gm),a=null,c=null,u=0,x=null,d={push:o,drain:me,saturated:me,pause:h,paused:!1,get concurrency(){return r},set concurrency(A){if(!(A>=1))throw new Error("fastqueue concurrency must be equal to or greater than 1");if(r=A,!d.paused)for(;a&&u=r||d.paused?c?(c.next=k,c=k):(a=k,c=k,d.saturated()):(u++,e.call(t,k.value,k.worked))}function f(A,T){var k=i.get();k.context=t,k.release=E,k.value=A,k.callback=T||me,k.errorHandler=x,u>=r||d.paused?a?(k.next=a,a=k):(a=k,c=k,d.saturated()):(u++,e.call(t,k.value,k.worked))}function E(A){A&&i.release(A);var T=a;T&&u<=r?d.paused?u--:(c===a&&(c=null),a=T.next,T.next=null,e.call(t,T.value,T.worked),c===null&&d.empty()):--u===0&&d.drain()}function g(){a=null,c=null,d.drain=me}function v(){a=null,c=null,d.drain(),d.drain=me}function F(A){x=A}}function me(){}function gm(){this.value=null,this.callback=me,this.next=null,this.release=me,this.context=null,this.errorHandler=null;var t=this;this.worked=function(r,i){var a=t.callback,c=t.errorHandler,u=t.value;t.value=null,t.callback=me,t.errorHandler&&c(r,u),a.call(t.context,r,i),t.release(t)}}function Dm(t,e,r){typeof t=="function"&&(r=e,e=t,t=null);function i(h,m){e.call(this,h).then(function(l){m(null,l)},m)}var a=Xu(t,i,r),c=a.push,u=a.unshift;return a.push=x,a.unshift=d,a.drained=p,a;function x(h){var m=new Promise(function(l,n){c(h,function(s,o){if(s){n(s);return}l(o)})});return m.catch(me),m}function d(h){var m=new Promise(function(l,n){u(h,function(s,o){if(s){n(s);return}l(o)})});return m.catch(me),m}function p(){if(a.idle())return new Promise(function(l){l()});var h=a.drain,m=new Promise(function(l){a.drain=function(){h(),l()}});return m}}Gi.exports=Xu;Gi.exports.promise=Dm});var cr=_(be=>{"use strict";Object.defineProperty(be,"__esModule",{value:!0});be.joinPathSegments=be.replacePathSegmentSeparator=be.isAppliedFilter=be.isFatalError=void 0;function ym(t,e){return t.errorFilter===null?!0:!t.errorFilter(e)}be.isFatalError=ym;function vm(t,e){return t===null||t(e)}be.isAppliedFilter=vm;function Sm(t,e){return t.split(/[/\\]/).join(e)}be.replacePathSegmentSeparator=Sm;function Am(t,e,r){return t===""?e:t.endsWith(r)?t+e:t+r+e}be.joinPathSegments=Am});var Vi=_(Wi=>{"use strict";Object.defineProperty(Wi,"__esModule",{value:!0});var Cm=cr(),Ki=class{constructor(e,r){this._root=e,this._settings=r,this._root=Cm.replacePathSegmentSeparator(e,r.pathSegmentSeparator)}};Wi.default=Ki});var Zi=_(Qi=>{"use strict";Object.defineProperty(Qi,"__esModule",{value:!0});var bm=O("events"),Fm=ur(),wm=Uu(),lr=cr(),_m=Vi(),Yi=class extends _m.default{constructor(e,r){super(e,r),this._settings=r,this._scandir=Fm.scandir,this._emitter=new bm.EventEmitter,this._queue=wm(this._worker.bind(this),this._settings.concurrency),this._isFatalError=!1,this._isDestroyed=!1,this._queue.drain=()=>{this._isFatalError||this._emitter.emit("end")}}read(){return this._isFatalError=!1,this._isDestroyed=!1,setImmediate(()=>{this._pushToQueue(this._root,this._settings.basePath)}),this._emitter}get isDestroyed(){return this._isDestroyed}destroy(){if(this._isDestroyed)throw new Error("The reader is already destroyed");this._isDestroyed=!0,this._queue.killAndDrain()}onEntry(e){this._emitter.on("entry",e)}onError(e){this._emitter.once("error",e)}onEnd(e){this._emitter.once("end",e)}_pushToQueue(e,r){let i={directory:e,base:r};this._queue.push(i,a=>{a!==null&&this._handleError(a)})}_worker(e,r){this._scandir(e.directory,this._settings.fsScandirSettings,(i,a)=>{if(i!==null){r(i,void 0);return}for(let c of a)this._handleEntry(c,e.base);r(null,void 0)})}_handleError(e){this._isDestroyed||!lr.isFatalError(this._settings,e)||(this._isFatalError=!0,this._isDestroyed=!0,this._emitter.emit("error",e))}_handleEntry(e,r){if(this._isDestroyed||this._isFatalError)return;let i=e.path;r!==void 0&&(e.path=lr.joinPathSegments(r,e.name,this._settings.pathSegmentSeparator)),lr.isAppliedFilter(this._settings.entryFilter,e)&&this._emitEntry(e),e.dirent.isDirectory()&&lr.isAppliedFilter(this._settings.deepFilter,e)&&this._pushToQueue(i,r===void 0?void 0:e.path)}_emitEntry(e){this._emitter.emit("entry",e)}};Qi.default=Yi});var $u=_(tn=>{"use strict";Object.defineProperty(tn,"__esModule",{value:!0});var km=Zi(),en=class{constructor(e,r){this._root=e,this._settings=r,this._reader=new km.default(this._root,this._settings),this._storage=[]}read(e){this._reader.onError(r=>{Tm(e,r)}),this._reader.onEntry(r=>{this._storage.push(r)}),this._reader.onEnd(()=>{Bm(e,this._storage)}),this._reader.read()}};tn.default=en;function Tm(t,e){t(e)}function Bm(t,e){t(null,e)}});var ju=_(nn=>{"use strict";Object.defineProperty(nn,"__esModule",{value:!0});var Pm=O("stream"),Rm=Zi(),rn=class{constructor(e,r){this._root=e,this._settings=r,this._reader=new Rm.default(this._root,this._settings),this._stream=new Pm.Readable({objectMode:!0,read:()=>{},destroy:()=>{this._reader.isDestroyed||this._reader.destroy()}})}read(){return this._reader.onError(e=>{this._stream.emit("error",e)}),this._reader.onEntry(e=>{this._stream.push(e)}),this._reader.onEnd(()=>{this._stream.push(null)}),this._reader.read(),this._stream}};nn.default=rn});var Ju=_(an=>{"use strict";Object.defineProperty(an,"__esModule",{value:!0});var Im=ur(),hr=cr(),Nm=Vi(),sn=class extends Nm.default{constructor(){super(...arguments),this._scandir=Im.scandirSync,this._storage=[],this._queue=new Set}read(){return this._pushToQueue(this._root,this._settings.basePath),this._handleQueue(),this._storage}_pushToQueue(e,r){this._queue.add({directory:e,base:r})}_handleQueue(){for(let e of this._queue.values())this._handleDirectory(e.directory,e.base)}_handleDirectory(e,r){try{let i=this._scandir(e,this._settings.fsScandirSettings);for(let a of i)this._handleEntry(a,r)}catch(i){this._handleError(i)}}_handleError(e){if(hr.isFatalError(this._settings,e))throw e}_handleEntry(e,r){let i=e.path;r!==void 0&&(e.path=hr.joinPathSegments(r,e.name,this._settings.pathSegmentSeparator)),hr.isAppliedFilter(this._settings.entryFilter,e)&&this._pushToStorage(e),e.dirent.isDirectory()&&hr.isAppliedFilter(this._settings.deepFilter,e)&&this._pushToQueue(i,r===void 0?void 0:e.path)}_pushToStorage(e){this._storage.push(e)}};an.default=sn});var zu=_(un=>{"use strict";Object.defineProperty(un,"__esModule",{value:!0});var Lm=Ju(),on=class{constructor(e,r){this._root=e,this._settings=r,this._reader=new Lm.default(this._root,this._settings)}read(){return this._reader.read()}};un.default=on});var qu=_(ln=>{"use strict";Object.defineProperty(ln,"__esModule",{value:!0});var Om=O("path"),Mm=ur(),cn=class{constructor(e={}){this._options=e,this.basePath=this._getValue(this._options.basePath,void 0),this.concurrency=this._getValue(this._options.concurrency,Number.POSITIVE_INFINITY),this.deepFilter=this._getValue(this._options.deepFilter,null),this.entryFilter=this._getValue(this._options.entryFilter,null),this.errorFilter=this._getValue(this._options.errorFilter,null),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,Om.sep),this.fsScandirSettings=new Mm.Settings({followSymbolicLinks:this._options.followSymbolicLinks,fs:this._options.fs,pathSegmentSeparator:this._options.pathSegmentSeparator,stats:this._options.stats,throwErrorOnBrokenSymbolicLink:this._options.throwErrorOnBrokenSymbolicLink})}_getValue(e,r){return e??r}};ln.default=cn});var pr=_(Fe=>{"use strict";Object.defineProperty(Fe,"__esModule",{value:!0});Fe.Settings=Fe.walkStream=Fe.walkSync=Fe.walk=void 0;var Gu=$u(),Hm=ju(),Xm=zu(),hn=qu();Fe.Settings=hn.default;function Um(t,e,r){if(typeof e=="function"){new Gu.default(t,fr()).read(e);return}new Gu.default(t,fr(e)).read(r)}Fe.walk=Um;function $m(t,e){let r=fr(e);return new Xm.default(t,r).read()}Fe.walkSync=$m;function jm(t,e){let r=fr(e);return new Hm.default(t,r).read()}Fe.walkStream=jm;function fr(t={}){return t instanceof hn.default?t:new hn.default(t)}});var dr=_(pn=>{"use strict";Object.defineProperty(pn,"__esModule",{value:!0});var Jm=O("path"),zm=Ge(),Ku=Te(),fn=class{constructor(e){this._settings=e,this._fsStatSettings=new zm.Settings({followSymbolicLink:this._settings.followSymbolicLinks,fs:this._settings.fs,throwErrorOnBrokenSymbolicLink:this._settings.followSymbolicLinks})}_getFullEntryPath(e){return Jm.resolve(this._settings.cwd,e)}_makeEntry(e,r){let i={name:r,path:r,dirent:Ku.fs.createDirentFromStats(r,e)};return this._settings.stats&&(i.stats=e),i}_isFatalError(e){return!Ku.errno.isEnoentCodeError(e)&&!this._settings.suppressErrors}};pn.default=fn});var xn=_(mn=>{"use strict";Object.defineProperty(mn,"__esModule",{value:!0});var qm=O("stream"),Gm=Ge(),Km=pr(),Wm=dr(),dn=class extends Wm.default{constructor(){super(...arguments),this._walkStream=Km.walkStream,this._stat=Gm.stat}dynamic(e,r){return this._walkStream(e,r)}static(e,r){let i=e.map(this._getFullEntryPath,this),a=new qm.PassThrough({objectMode:!0});a._write=(c,u,x)=>this._getEntry(i[c],e[c],r).then(d=>{d!==null&&r.entryFilter(d)&&a.push(d),c===i.length-1&&a.end(),x()}).catch(x);for(let c=0;cthis._makeEntry(a,r)).catch(a=>{if(i.errorFilter(a))return null;throw a})}_getStat(e){return new Promise((r,i)=>{this._stat(e,this._fsStatSettings,(a,c)=>a===null?r(c):i(a))})}};mn.default=dn});var Wu=_(gn=>{"use strict";Object.defineProperty(gn,"__esModule",{value:!0});var Vm=pr(),Ym=dr(),Qm=xn(),En=class extends Ym.default{constructor(){super(...arguments),this._walkAsync=Vm.walk,this._readerStream=new Qm.default(this._settings)}dynamic(e,r){return new Promise((i,a)=>{this._walkAsync(e,r,(c,u)=>{c===null?i(u):a(c)})})}async static(e,r){let i=[],a=this._readerStream.static(e,r);return new Promise((c,u)=>{a.once("error",u),a.on("data",x=>i.push(x)),a.once("end",()=>c(i))})}};gn.default=En});var Vu=_(yn=>{"use strict";Object.defineProperty(yn,"__esModule",{value:!0});var bt=Te(),Dn=class{constructor(e,r,i){this._patterns=e,this._settings=r,this._micromatchOptions=i,this._storage=[],this._fillStorage()}_fillStorage(){for(let e of this._patterns){let r=this._getPatternSegments(e),i=this._splitSegmentsIntoSections(r);this._storage.push({complete:i.length<=1,pattern:e,segments:r,sections:i})}}_getPatternSegments(e){return bt.pattern.getPatternParts(e,this._micromatchOptions).map(i=>bt.pattern.isDynamicPattern(i,this._settings)?{dynamic:!0,pattern:i,patternRe:bt.pattern.makeRe(i,this._micromatchOptions)}:{dynamic:!1,pattern:i})}_splitSegmentsIntoSections(e){return bt.array.splitWhen(e,r=>r.dynamic&&bt.pattern.hasGlobStar(r.pattern))}};yn.default=Dn});var Yu=_(Sn=>{"use strict";Object.defineProperty(Sn,"__esModule",{value:!0});var Zm=Vu(),vn=class extends Zm.default{match(e){let r=e.split("/"),i=r.length,a=this._storage.filter(c=>!c.complete||c.segments.length>i);for(let c of a){let u=c.sections[0];if(!c.complete&&i>u.length||r.every((d,p)=>{let h=c.segments[p];return!!(h.dynamic&&h.patternRe.test(d)||!h.dynamic&&h.pattern===d)}))return!0}return!1}};Sn.default=vn});var Qu=_(Cn=>{"use strict";Object.defineProperty(Cn,"__esModule",{value:!0});var mr=Te(),e0=Yu(),An=class{constructor(e,r){this._settings=e,this._micromatchOptions=r}getFilter(e,r,i){let a=this._getMatcher(r),c=this._getNegativePatternsRe(i);return u=>this._filter(e,u,a,c)}_getMatcher(e){return new e0.default(e,this._settings,this._micromatchOptions)}_getNegativePatternsRe(e){let r=e.filter(mr.pattern.isAffectDepthOfReadingPattern);return mr.pattern.convertPatternsToRe(r,this._micromatchOptions)}_filter(e,r,i,a){if(this._isSkippedByDeep(e,r.path)||this._isSkippedSymbolicLink(r))return!1;let c=mr.path.removeLeadingDotSegment(r.path);return this._isSkippedByPositivePatterns(c,i)?!1:this._isSkippedByNegativePatterns(c,a)}_isSkippedByDeep(e,r){return this._settings.deep===1/0?!1:this._getEntryLevel(e,r)>=this._settings.deep}_getEntryLevel(e,r){let i=r.split("/").length;if(e==="")return i;let a=e.split("/").length;return i-a}_isSkippedSymbolicLink(e){return!this._settings.followSymbolicLinks&&e.dirent.isSymbolicLink()}_isSkippedByPositivePatterns(e,r){return!this._settings.baseNameMatch&&!r.match(e)}_isSkippedByNegativePatterns(e,r){return!mr.pattern.matchAny(e,r)}};Cn.default=An});var Zu=_(Fn=>{"use strict";Object.defineProperty(Fn,"__esModule",{value:!0});var Ke=Te(),bn=class{constructor(e,r){this._settings=e,this._micromatchOptions=r,this.index=new Map}getFilter(e,r){let i=Ke.pattern.convertPatternsToRe(e,this._micromatchOptions),a=Ke.pattern.convertPatternsToRe(r,Object.assign(Object.assign({},this._micromatchOptions),{dot:!0}));return c=>this._filter(c,i,a)}_filter(e,r,i){let a=Ke.path.removeLeadingDotSegment(e.path);if(this._settings.unique&&this._isDuplicateEntry(a)||this._onlyFileFilter(e)||this._onlyDirectoryFilter(e)||this._isSkippedByAbsoluteNegativePatterns(a,i))return!1;let c=e.dirent.isDirectory(),u=this._isMatchToPatterns(a,r,c)&&!this._isMatchToPatterns(a,i,c);return this._settings.unique&&u&&this._createIndexRecord(a),u}_isDuplicateEntry(e){return this.index.has(e)}_createIndexRecord(e){this.index.set(e,void 0)}_onlyFileFilter(e){return this._settings.onlyFiles&&!e.dirent.isFile()}_onlyDirectoryFilter(e){return this._settings.onlyDirectories&&!e.dirent.isDirectory()}_isSkippedByAbsoluteNegativePatterns(e,r){if(!this._settings.absolute)return!1;let i=Ke.path.makeAbsolute(this._settings.cwd,e);return Ke.pattern.matchAny(i,r)}_isMatchToPatterns(e,r,i){let a=Ke.pattern.matchAny(e,r);return!a&&i?Ke.pattern.matchAny(e+"/",r):a}};Fn.default=bn});var ec=_(_n=>{"use strict";Object.defineProperty(_n,"__esModule",{value:!0});var t0=Te(),wn=class{constructor(e){this._settings=e}getFilter(){return e=>this._isNonFatalError(e)}_isNonFatalError(e){return t0.errno.isEnoentCodeError(e)||this._settings.suppressErrors}};_n.default=wn});var rc=_(Tn=>{"use strict";Object.defineProperty(Tn,"__esModule",{value:!0});var tc=Te(),kn=class{constructor(e){this._settings=e}getTransformer(){return e=>this._transform(e)}_transform(e){let r=e.path;return this._settings.absolute&&(r=tc.path.makeAbsolute(this._settings.cwd,r),r=tc.path.unixify(r)),this._settings.markDirectories&&e.dirent.isDirectory()&&(r+="/"),this._settings.objectMode?Object.assign(Object.assign({},e),{path:r}):r}};Tn.default=kn});var xr=_(Pn=>{"use strict";Object.defineProperty(Pn,"__esModule",{value:!0});var r0=O("path"),i0=Qu(),n0=Zu(),s0=ec(),a0=rc(),Bn=class{constructor(e){this._settings=e,this.errorFilter=new s0.default(this._settings),this.entryFilter=new n0.default(this._settings,this._getMicromatchOptions()),this.deepFilter=new i0.default(this._settings,this._getMicromatchOptions()),this.entryTransformer=new a0.default(this._settings)}_getRootDirectory(e){return r0.resolve(this._settings.cwd,e.base)}_getReaderOptions(e){let r=e.base==="."?"":e.base;return{basePath:r,pathSegmentSeparator:"/",concurrency:this._settings.concurrency,deepFilter:this.deepFilter.getFilter(r,e.positive,e.negative),entryFilter:this.entryFilter.getFilter(e.positive,e.negative),errorFilter:this.errorFilter.getFilter(),followSymbolicLinks:this._settings.followSymbolicLinks,fs:this._settings.fs,stats:this._settings.stats,throwErrorOnBrokenSymbolicLink:this._settings.throwErrorOnBrokenSymbolicLink,transform:this.entryTransformer.getTransformer()}}_getMicromatchOptions(){return{dot:this._settings.dot,matchBase:this._settings.baseNameMatch,nobrace:!this._settings.braceExpansion,nocase:!this._settings.caseSensitiveMatch,noext:!this._settings.extglob,noglobstar:!this._settings.globstar,posix:!0,strictSlashes:!1}}};Pn.default=Bn});var ic=_(In=>{"use strict";Object.defineProperty(In,"__esModule",{value:!0});var o0=Wu(),u0=xr(),Rn=class extends u0.default{constructor(){super(...arguments),this._reader=new o0.default(this._settings)}async read(e){let r=this._getRootDirectory(e),i=this._getReaderOptions(e);return(await this.api(r,e,i)).map(c=>i.transform(c))}api(e,r,i){return r.dynamic?this._reader.dynamic(e,i):this._reader.static(r.patterns,i)}};In.default=Rn});var nc=_(Ln=>{"use strict";Object.defineProperty(Ln,"__esModule",{value:!0});var c0=O("stream"),l0=xn(),h0=xr(),Nn=class extends h0.default{constructor(){super(...arguments),this._reader=new l0.default(this._settings)}read(e){let r=this._getRootDirectory(e),i=this._getReaderOptions(e),a=this.api(r,e,i),c=new c0.Readable({objectMode:!0,read:()=>{}});return a.once("error",u=>c.emit("error",u)).on("data",u=>c.emit("data",i.transform(u))).once("end",()=>c.emit("end")),c.once("close",()=>a.destroy()),c}api(e,r,i){return r.dynamic?this._reader.dynamic(e,i):this._reader.static(r.patterns,i)}};Ln.default=Nn});var sc=_(Mn=>{"use strict";Object.defineProperty(Mn,"__esModule",{value:!0});var f0=Ge(),p0=pr(),d0=dr(),On=class extends d0.default{constructor(){super(...arguments),this._walkSync=p0.walkSync,this._statSync=f0.statSync}dynamic(e,r){return this._walkSync(e,r)}static(e,r){let i=[];for(let a of e){let c=this._getFullEntryPath(a),u=this._getEntry(c,a,r);u===null||!r.entryFilter(u)||i.push(u)}return i}_getEntry(e,r,i){try{let a=this._getStat(e);return this._makeEntry(a,r)}catch(a){if(i.errorFilter(a))return null;throw a}}_getStat(e){return this._statSync(e,this._fsStatSettings)}};Mn.default=On});var ac=_(Xn=>{"use strict";Object.defineProperty(Xn,"__esModule",{value:!0});var m0=sc(),x0=xr(),Hn=class extends x0.default{constructor(){super(...arguments),this._reader=new m0.default(this._settings)}read(e){let r=this._getRootDirectory(e),i=this._getReaderOptions(e);return this.api(r,e,i).map(i.transform)}api(e,r,i){return r.dynamic?this._reader.dynamic(e,i):this._reader.static(r.patterns,i)}};Xn.default=Hn});var oc=_(ct=>{"use strict";Object.defineProperty(ct,"__esModule",{value:!0});ct.DEFAULT_FILE_SYSTEM_ADAPTER=void 0;var ut=O("fs"),E0=O("os"),g0=Math.max(E0.cpus().length,1);ct.DEFAULT_FILE_SYSTEM_ADAPTER={lstat:ut.lstat,lstatSync:ut.lstatSync,stat:ut.stat,statSync:ut.statSync,readdir:ut.readdir,readdirSync:ut.readdirSync};var Un=class{constructor(e={}){this._options=e,this.absolute=this._getValue(this._options.absolute,!1),this.baseNameMatch=this._getValue(this._options.baseNameMatch,!1),this.braceExpansion=this._getValue(this._options.braceExpansion,!0),this.caseSensitiveMatch=this._getValue(this._options.caseSensitiveMatch,!0),this.concurrency=this._getValue(this._options.concurrency,g0),this.cwd=this._getValue(this._options.cwd,process.cwd()),this.deep=this._getValue(this._options.deep,1/0),this.dot=this._getValue(this._options.dot,!1),this.extglob=this._getValue(this._options.extglob,!0),this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!0),this.fs=this._getFileSystemMethods(this._options.fs),this.globstar=this._getValue(this._options.globstar,!0),this.ignore=this._getValue(this._options.ignore,[]),this.markDirectories=this._getValue(this._options.markDirectories,!1),this.objectMode=this._getValue(this._options.objectMode,!1),this.onlyDirectories=this._getValue(this._options.onlyDirectories,!1),this.onlyFiles=this._getValue(this._options.onlyFiles,!0),this.stats=this._getValue(this._options.stats,!1),this.suppressErrors=this._getValue(this._options.suppressErrors,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!1),this.unique=this._getValue(this._options.unique,!0),this.onlyDirectories&&(this.onlyFiles=!1),this.stats&&(this.objectMode=!0),this.ignore=[].concat(this.ignore)}_getValue(e,r){return e===void 0?r:e}_getFileSystemMethods(e={}){return Object.assign(Object.assign({},ct.DEFAULT_FILE_SYSTEM_ADAPTER),e)}};ct.default=Un});var zn=_((_E,cc)=>{"use strict";var uc=cu(),D0=ic(),y0=nc(),v0=ac(),$n=oc(),xe=Te();async function jn(t,e){De(t);let r=Jn(t,D0.default,e),i=await Promise.all(r);return xe.array.flatten(i)}(function(t){t.glob=t,t.globSync=e,t.globStream=r,t.async=t;function e(p,h){De(p);let m=Jn(p,v0.default,h);return xe.array.flatten(m)}t.sync=e;function r(p,h){De(p);let m=Jn(p,y0.default,h);return xe.stream.merge(m)}t.stream=r;function i(p,h){De(p);let m=[].concat(p),l=new $n.default(h);return uc.generate(m,l)}t.generateTasks=i;function a(p,h){De(p);let m=new $n.default(h);return xe.pattern.isDynamicPattern(p,m)}t.isDynamicPattern=a;function c(p){return De(p),xe.path.escape(p)}t.escapePath=c;function u(p){return De(p),xe.path.convertPathToPattern(p)}t.convertPathToPattern=u;let x;(function(p){function h(l){return De(l),xe.path.escapePosixPath(l)}p.escapePath=h;function m(l){return De(l),xe.path.convertPosixPathToPattern(l)}p.convertPathToPattern=m})(x=t.posix||(t.posix={}));let d;(function(p){function h(l){return De(l),xe.path.escapeWindowsPath(l)}p.escapePath=h;function m(l){return De(l),xe.path.convertWindowsPathToPattern(l)}p.convertPathToPattern=m})(d=t.win32||(t.win32={}))})(jn||(jn={}));function Jn(t,e,r){let i=[].concat(t),a=new $n.default(r),c=uc.generate(i,a),u=new e(a);return c.map(u.read,u)}function De(t){if(![].concat(t).every(i=>xe.string.isString(i)&&!xe.string.isEmpty(i)))throw new TypeError("Patterns must be a string (non empty) or an array of strings")}cc.exports=jn});var vc=_((LE,yc)=>{function pc(t){return Array.isArray(t)?t:[t]}var Vn="",dc=" ",Kn="\\",S0=/^\s+$/,A0=/(?:[^\\]|^)\\$/,C0=/^\\!/,b0=/^\\#/,F0=/\r?\n/g,w0=/^\.*\/|^\.+$/,Wn="/",Ec="node-ignore";typeof Symbol<"u"&&(Ec=Symbol.for("node-ignore"));var mc=Ec,_0=(t,e,r)=>Object.defineProperty(t,e,{value:r}),k0=/([0-z])-([0-z])/g,gc=()=>!1,T0=t=>t.replace(k0,(e,r,i)=>r.charCodeAt(0)<=i.charCodeAt(0)?e:Vn),B0=t=>{let{length:e}=t;return t.slice(0,e-e%2)},P0=[[/^\uFEFF/,()=>Vn],[/((?:\\\\)*?)(\\?\s+)$/,(t,e,r)=>e+(r.indexOf("\\")===0?dc:Vn)],[/(\\+?)\s/g,(t,e)=>{let{length:r}=e;return e.slice(0,r-r%2)+dc}],[/[\\$.|*+(){^]/g,t=>`\\${t}`],[/(?!\\)\?/g,()=>"[^/]"],[/^\//,()=>"^"],[/\//g,()=>"\\/"],[/^\^*\\\*\\\*\\\//,()=>"^(?:.*\\/)?"],[/^(?=[^^])/,function(){return/\/(?!$)/.test(this)?"^":"(?:^|\\/)"}],[/\\\/\\\*\\\*(?=\\\/|$)/g,(t,e,r)=>e+6{let i=r.replace(/\\\*/g,"[^\\/]*");return e+i}],[/\\\\\\(?=[$.|*+(){^])/g,()=>Kn],[/\\\\/g,()=>Kn],[/(\\)?\[([^\]/]*?)(\\*)($|\])/g,(t,e,r,i,a)=>e===Kn?`\\[${r}${B0(i)}${a}`:a==="]"&&i.length%2===0?`[${T0(r)}${i}]`:"[]"],[/(?:[^*])$/,t=>/\/$/.test(t)?`${t}$`:`${t}(?=$|\\/$)`],[/(\^|\\\/)?\\\*$/,(t,e)=>`${e?`${e}[^/]+`:"[^/]*"}(?=$|\\/$)`]],xc=Object.create(null),R0=(t,e)=>{let r=xc[t];return r||(r=P0.reduce((i,[a,c])=>i.replace(a,c.bind(t)),t),xc[t]=r),e?new RegExp(r,"i"):new RegExp(r)},Zn=t=>typeof t=="string",I0=t=>t&&Zn(t)&&!S0.test(t)&&!A0.test(t)&&t.indexOf("#")!==0,N0=t=>t.split(F0),Yn=class{constructor(e,r,i,a){this.origin=e,this.pattern=r,this.negative=i,this.regex=a}},L0=(t,e)=>{let r=t,i=!1;t.indexOf("!")===0&&(i=!0,t=t.substr(1)),t=t.replace(C0,"!").replace(b0,"#");let a=R0(t,e);return new Yn(r,t,i,a)},O0=(t,e)=>{throw new e(t)},Be=(t,e,r)=>Zn(t)?t?Be.isNotRelative(t)?r(`path should be a \`path.relative()\`d string, but got "${e}"`,RangeError):!0:r("path must not be empty",TypeError):r(`path must be a string, but got \`${e}\``,TypeError),Dc=t=>w0.test(t);Be.isNotRelative=Dc;Be.convert=t=>t;var Qn=class{constructor({ignorecase:e=!0,ignoreCase:r=e,allowRelativePaths:i=!1}={}){_0(this,mc,!0),this._rules=[],this._ignoreCase=r,this._allowRelativePaths=i,this._initCache()}_initCache(){this._ignoreCache=Object.create(null),this._testCache=Object.create(null)}_addPattern(e){if(e&&e[mc]){this._rules=this._rules.concat(e._rules),this._added=!0;return}if(I0(e)){let r=L0(e,this._ignoreCase);this._added=!0,this._rules.push(r)}}add(e){return this._added=!1,pc(Zn(e)?N0(e):e).forEach(this._addPattern,this),this._added&&this._initCache(),this}addPattern(e){return this.add(e)}_testOne(e,r){let i=!1,a=!1;return this._rules.forEach(c=>{let{negative:u}=c;if(a===u&&i!==a||u&&!i&&!a&&!r)return;c.regex.test(e)&&(i=!u,a=u)}),{ignored:i,unignored:a}}_test(e,r,i,a){let c=e&&Be.convert(e);return Be(c,e,this._allowRelativePaths?gc:O0),this._t(c,r,i,a)}_t(e,r,i,a){if(e in r)return r[e];if(a||(a=e.split(Wn)),a.pop(),!a.length)return r[e]=this._testOne(e,i);let c=this._t(a.join(Wn)+Wn,r,i,a);return r[e]=c.ignored?c:this._testOne(e,i)}ignores(e){return this._test(e,this._ignoreCache,!1).ignored}createFilter(){return e=>!this.ignores(e)}filter(e){return pc(e).filter(this.createFilter())}test(e){return this._test(e,this._testCache,!0)}},gr=t=>new Qn(t),M0=t=>Be(t&&Be.convert(t),t,gc);gr.isPathValid=M0;gr.default=gr;yc.exports=gr;if(typeof process<"u"&&(process.env&&process.env.IGNORE_TEST_WIN32||process.platform==="win32")){let t=r=>/^\\\\\?\\/.test(r)||/["<>|\u0000-\u001F]+/u.test(r)?r:r.replace(/\\/g,"/");Be.convert=t;let e=/^[a-z]:\//i;Be.isNotRelative=r=>e.test(r)||Dc(r)}});var V0={};Fl(V0,{default:()=>W0});var Ue=O("path");var $e=O("path");function os(t){t=t.replace(/\\/g,"/");let e=t,r=[t];return t.endsWith(".json")?(r=[t],e=$e.posix.join(t,"..","package.json"),t=$e.posix.join(t,"..")):(r=[$e.posix.join(t,"tsconfig.json"),$e.posix.join(t,"tsconfig.*.json")],e=$e.posix.join(t,"package.json")),{directory:$e.posix.resolve(t),packageJsonPath:e,tsConfigPatterns:r}}var _t=O("fs/promises"),Ee=ne(ls(),1),ft=ne(ti(),1);var ts=ne(O("process"),1),Ic=ne(O("fs"),1),Ve=ne(O("path"),1);var Ut=O("events"),Ea=O("stream"),si=O("stream/promises");function ai(t){if(!Array.isArray(t))throw new TypeError(`Expected an array, got \`${typeof t}\`.`);for(let a of t)ii(a);let e=t.some(({readableObjectMode:a})=>a),r=uf(t,e),i=new ri({objectMode:e,writableHighWaterMark:r,readableHighWaterMark:r});for(let a of t)i.add(a);return t.length===0&&ya(i),i}var uf=(t,e)=>{if(t.length===0)return 16384;let r=t.filter(({readableObjectMode:i})=>i===e).map(({readableHighWaterMark:i})=>i);return Math.max(...r)},ri=class extends Ea.PassThrough{#e=new Set([]);#r=new Set([]);#i=new Set([]);#t;add(e){ii(e),!this.#e.has(e)&&(this.#e.add(e),this.#t??=cf(this,this.#e),ff({passThroughStream:this,stream:e,streams:this.#e,ended:this.#r,aborted:this.#i,onFinished:this.#t}),e.pipe(this,{end:!1}))}remove(e){return ii(e),this.#e.has(e)?(e.unpipe(this),!0):!1}},cf=async(t,e)=>{Xt(t,ma);let r=new AbortController;try{await Promise.race([lf(t,r),hf(t,e,r)])}finally{r.abort(),Xt(t,-ma)}},lf=async(t,{signal:e})=>{await(0,si.finished)(t,{signal:e,cleanup:!0})},hf=async(t,e,{signal:r})=>{for await(let[i]of(0,Ut.on)(t,"unpipe",{signal:r}))e.has(i)&&i.emit(Da)},ii=t=>{if(typeof t?.pipe!="function")throw new TypeError(`Expected a readable stream, got: \`${typeof t}\`.`)},ff=async({passThroughStream:t,stream:e,streams:r,ended:i,aborted:a,onFinished:c})=>{Xt(t,xa);let u=new AbortController;try{await Promise.race([pf(c,e),df({passThroughStream:t,stream:e,streams:r,ended:i,aborted:a,controller:u}),mf({stream:e,streams:r,ended:i,aborted:a,controller:u})])}finally{u.abort(),Xt(t,-xa)}r.size===i.size+a.size&&(i.size===0&&a.size>0?ni(t):ya(t))},ga=t=>t?.code==="ERR_STREAM_PREMATURE_CLOSE",pf=async(t,e)=>{try{await t,ni(e)}catch(r){ga(r)?ni(e):va(e,r)}},df=async({passThroughStream:t,stream:e,streams:r,ended:i,aborted:a,controller:{signal:c}})=>{try{await(0,si.finished)(e,{signal:c,cleanup:!0,readable:!0,writable:!1}),r.has(e)&&i.add(e)}catch(u){if(c.aborted||!r.has(e))return;ga(u)?a.add(e):va(t,u)}},mf=async({stream:t,streams:e,ended:r,aborted:i,controller:{signal:a}})=>{await(0,Ut.once)(t,Da,{signal:a}),e.delete(t),r.delete(t),i.delete(t)},Da=Symbol("unpipe"),ya=t=>{t.writable&&t.end()},ni=t=>{(t.readable||t.writable)&&t.destroy()},va=(t,e)=>{t.destroyed||(t.once("error",xf),t.destroy(e))},xf=()=>{},Xt=(t,e)=>{let r=t.getMaxListeners();r!==0&&r!==Number.POSITIVE_INFINITY&&t.setMaxListeners(r+e)},ma=2,xa=1;var ht=ne(zn(),1);var Er=ne(O("fs"),1);async function qn(t,e,r){if(typeof r!="string")throw new TypeError(`Expected a string, got ${typeof r}`);try{return(await Er.promises[t](r))[e]()}catch(i){if(i.code==="ENOENT")return!1;throw i}}function Gn(t,e,r){if(typeof r!="string")throw new TypeError(`Expected a string, got ${typeof r}`);try{return Er.default[t](r)[e]()}catch(i){if(i.code==="ENOENT")return!1;throw i}}var kE=qn.bind(null,"stat","isFile"),lc=qn.bind(null,"stat","isDirectory"),TE=qn.bind(null,"lstat","isSymbolicLink"),BE=Gn.bind(null,"statSync","isFile"),hc=Gn.bind(null,"statSync","isDirectory"),PE=Gn.bind(null,"lstatSync","isSymbolicLink");var fc=O("url");function Ft(t){return t instanceof URL?(0,fc.fileURLToPath)(t):t}var Sc=ne(O("process"),1),Ac=ne(O("fs"),1),Cc=ne(O("fs/promises"),1),We=ne(O("path"),1),es=ne(zn(),1),bc=ne(vc(),1);function lt(t){return t.startsWith("\\\\?\\")?t:t.replace(/\\/g,"/")}var wt=t=>t[0]==="!";var H0=["**/node_modules","**/flow-typed","**/coverage","**/.git"],Fc={absolute:!0,dot:!0},wc="**/.gitignore",X0=(t,e)=>wt(t)?"!"+We.default.posix.join(e,t.slice(1)):We.default.posix.join(e,t),U0=(t,e)=>{let r=lt(We.default.relative(e,We.default.dirname(t.filePath)));return t.content.split(/\r?\n/).filter(i=>i&&!i.startsWith("#")).map(i=>X0(i,r))},$0=(t,e)=>{if(e=lt(e),We.default.isAbsolute(t)){if(lt(t).startsWith(e))return We.default.relative(e,t);throw new Error(`Path ${t} is not in cwd ${e}`)}return t},_c=(t,e)=>{let r=t.flatMap(a=>U0(a,e)),i=(0,bc.default)().add(r);return a=>(a=Ft(a),a=$0(a,e),a?i.ignores(lt(a)):!1)},kc=(t={})=>({cwd:Ft(t.cwd)??Sc.default.cwd(),suppressErrors:!!t.suppressErrors,deep:typeof t.deep=="number"?t.deep:Number.POSITIVE_INFINITY,ignore:[...t.ignore??[],...H0]}),Tc=async(t,e)=>{let{cwd:r,suppressErrors:i,deep:a,ignore:c}=kc(e),u=await(0,es.default)(t,{cwd:r,suppressErrors:i,deep:a,ignore:c,...Fc}),x=await Promise.all(u.map(async d=>({filePath:d,content:await Cc.default.readFile(d,"utf8")})));return _c(x,r)},Bc=(t,e)=>{let{cwd:r,suppressErrors:i,deep:a,ignore:c}=kc(e),x=es.default.sync(t,{cwd:r,suppressErrors:i,deep:a,ignore:c,...Fc}).map(d=>({filePath:d,content:Ac.default.readFileSync(d,"utf8")}));return _c(x,r)};var j0=t=>{if(t.some(e=>typeof e!="string"))throw new TypeError("Patterns must be a string or an array of strings")},Nc=(t,e)=>{let r=wt(t)?t.slice(1):t;return Ve.default.isAbsolute(r)?r:Ve.default.join(e,r)},Lc=({directoryPath:t,files:e,extensions:r})=>{let i=r?.length>0?`.${r.length>1?`{${r.join(",")}}`:r[0]}`:"";return e?e.map(a=>Ve.default.posix.join(t,`**/${Ve.default.extname(a)?a:`${a}${i}`}`)):[Ve.default.posix.join(t,`**${i?`/*${i}`:""}`)]},Pc=async(t,{cwd:e=ts.default.cwd(),files:r,extensions:i}={})=>(await Promise.all(t.map(async c=>await lc(Nc(c,e))?Lc({directoryPath:c,files:r,extensions:i}):c))).flat(),Rc=(t,{cwd:e=ts.default.cwd(),files:r,extensions:i}={})=>t.flatMap(a=>hc(Nc(a,e))?Lc({directoryPath:a,files:r,extensions:i}):a),rs=t=>(t=[...new Set([t].flat())],j0(t),t),J0=t=>{if(!t)return;let e;try{e=Ic.default.statSync(t)}catch{return}if(!e.isDirectory())throw new Error("The `cwd` option must be a path to a directory")},Oc=(t={})=>(t={...t,ignore:t.ignore??[],expandDirectories:t.expandDirectories??!0,cwd:Ft(t.cwd)},J0(t.cwd),t),Mc=t=>async(e,r)=>t(rs(e),Oc(r)),Dr=t=>(e,r)=>t(rs(e),Oc(r)),Hc=t=>{let{ignoreFiles:e,gitignore:r}=t,i=e?rs(e):[];return r&&i.push(wc),i},z0=async t=>{let e=Hc(t);return Uc(e.length>0&&await Tc(e,t))},Xc=t=>{let e=Hc(t);return Uc(e.length>0&&Bc(e,t))},Uc=t=>{let e=new Set;return r=>{let i=Ve.default.normalize(r.path??r);return e.has(i)||t&&t(i)?!1:(e.add(i),!0)}},$c=(t,e)=>t.flat().filter(r=>e(r)),jc=(t,e)=>{let r=[];for(;t.length>0;){let i=t.findIndex(c=>wt(c));if(i===-1){r.push({patterns:t,options:e});break}let a=t[i].slice(1);for(let c of r)c.options.ignore.push(a);i!==0&&r.push({patterns:t.slice(0,i),options:{...e,ignore:[...e.ignore,a]}}),t=t.slice(i+1)}return r},Jc=(t,e)=>({...e?{cwd:e}:{},...Array.isArray(t)?{files:t}:t}),zc=async(t,e)=>{let r=jc(t,e),{cwd:i,expandDirectories:a}=e;if(!a)return r;let c=Jc(a,i);return Promise.all(r.map(async u=>{let{patterns:x,options:d}=u;return[x,d.ignore]=await Promise.all([Pc(x,c),Pc(d.ignore,{cwd:i})]),{patterns:x,options:d}}))},is=(t,e)=>{let r=jc(t,e),{cwd:i,expandDirectories:a}=e;if(!a)return r;let c=Jc(a,i);return r.map(u=>{let{patterns:x,options:d}=u;return x=Rc(x,c),d.ignore=Rc(d.ignore,{cwd:i}),{patterns:x,options:d}})},qc=Mc(async(t,e)=>{let[r,i]=await Promise.all([zc(t,e),z0(e)]),a=await Promise.all(r.map(c=>(0,ht.default)(c.patterns,c.options)));return $c(a,i)}),KE=Dr((t,e)=>{let r=is(t,e),i=Xc(e),a=r.map(c=>ht.default.sync(c.patterns,c.options));return $c(a,i)}),WE=Dr((t,e)=>{let r=is(t,e),i=Xc(e),a=r.map(u=>ht.default.stream(u.patterns,u.options));return ai(a).filter(u=>i(u))}),VE=Dr((t,e)=>t.some(r=>ht.default.isDynamicPattern(r,e))),YE=Mc(zc),QE=Dr(is),{convertPathToPattern:ZE}=ht.default;var ss=ne(O("os"),1);var Gc=O("fs/promises"),Sr=ne(ti(),1),ns=O("path");function yr(t){return t.endsWith("tsconfig.json")}var vr=class{cache;constructor(){this.cache=new Map}async resolve(e){if(this.cache.has(e))return this.cache.get(e);let r=await(0,Gc.readFile)(e,"utf-8").then(i=>Sr.default.parse(i));for(let i of r.references||[])(0,Sr.assign)(i,{path:ns.posix.join(e,"..",i.path)});return this.cache.set(e,r),r}async isReferencedInRootConfig(e){return yr(e)?!1:(await this.resolve(ns.posix.join(e,"..","tsconfig.json"))).references?.some(i=>i.path===e)||!1}};var Wc=O("fs");function Kc(t){return t.replace(/\\/g,"/")}async function Vc(t={},...e){let r=new vr,i=new Map,a=new Map;for(let u of e){let{directory:x,packageJsonPath:d,tsConfigPatterns:p}=os(u);try{let h=await(0,_t.readFile)(d,"utf-8").then(n=>JSON.parse(n));if(!h.name){console.warn(`No name found in package.json at ${d}, skipping`);continue}let m=await qc(p);if(m.length===0)continue;let l={directory:x,tsConfigPaths:m,packageInfo:{name:h.name,dependencies:{...h.dependencies||{},...h.devDependencies||{}}}};a.set(h.name,l),i.set(x,l);for(let n of m)i.set(n,l)}catch{console.warn(`Error reading package.json at ${d}, skipping`)}}let c=!1;for(let[,{directory:u,tsConfigPaths:x,packageInfo:d}]of a)for(let p of x)try{let h=await r.resolve(p),m=new Set,l=new Set,n=[],s=new Set;for(let E of h.references||[]){let g=E.path.endsWith(".json")?E.path:Ue.posix.join(E.path,"tsconfig.json"),v=i.get(g);if(v){if(await r.isReferencedInRootConfig(g)&&!yr(p)){n.push({name:Ue.posix.relative(u,E.path),referencedInRoot:!0});continue}let A=await r.resolve(g),T=!A?.compilerOptions?.composite,k=A?.compilerOptions?.noEmit;if(!(v.packageInfo.name in d.dependencies||v.directory===u)||T||k){n.push({name:v.packageInfo.name,notComposite:T,noEmit:k});continue}}else if((0,Wc.existsSync)(g))m.add(Ue.posix.relative(u,E.path));else{n.push({name:Ue.posix.relative(u,E.path),notFound:!0});continue}s.add(Ue.posix.relative(u,E.path))}for(let E of Object.keys(d.dependencies)){let g=a.get(E);if(g)for(let v of g.tsConfigPaths){if(await r.isReferencedInRootConfig(v))continue;let A=await r.resolve(v);if(!A.compilerOptions?.composite||A.compilerOptions?.noEmit)continue;let T=Ue.posix.relative(u,v.replace("tsconfig.json",""));s.has(T)||(s.add(T),l.add(E))}}if((l.size||m.size||n.length)&&console.log(q0(d.name)+Ee.default.dim(" \xB7"),Ee.default.dim(Ue.posix.relative(Kc(process.cwd()),p))),l.size)for(let E of l)console.log(Ee.default.greenBright(" + ")+Ee.default.dim(`${E}`));if(n.length)for(let E of n){let g="";t.verbose&&(E.noEmit&&(g+=" (noEmit: true)"),E.notComposite&&(g+=" (composite: false)"),E.notFound&&(g+=" (not found)"),E.referencedInRoot&&(g+=" (referenced in root tsconfig.json)")),console.log(Ee.default.redBright(" - ")+Ee.default.dim(`${E.name}${g}`))}if(m.size)for(let E of m)console.log(Ee.default.yellowBright(" ? ")+Ee.default.dim(`${E}`));if(t.immutable){(l.size||n.length)&&(c=!0);continue}let o=await(0,_t.readFile)(p,"utf-8").then(E=>ft.default.parse(E)),f=o.references?.filter(E=>s.has(E.path))||[];for(let E of s)f.some(g=>g.path===E)||f.push({path:E});(0,ft.assign)(o,{references:f.sort((E,g)=>E.path.localeCompare(g.path))}),o.references?.length===0&&delete o.references,await(0,_t.writeFile)(p,G0((0,ft.stringify)(o,null,2))+ss.default.EOL)}catch(h){console.warn(`Error reading tsconfig.json at ${p}, skipping`,h);continue}if(t.verbose){let u=a.size;console.log(Ee.default.dim(Ee.default.greenBright(`Linked ${u} package${u===1?"":"s"}`)))}c&&(console.error("Immutable mode is enabled, please fix the issues manually"),process.exit(1))}function q0(t){let e="\x1B[38;2;200;85;15m",r="\x1B[38;2;200;130;90m";return t.replace(/^(@[^\/]+\/)?(.*?)$/,(i,a,c)=>`${e}${a||""}${r}${c}${e}`+Ee.default.reset(""))}function G0(t){let e=/\r\n|\n|\r/g;return t.replace(e,ss.default.EOL)}var Yc=O("@yarnpkg/fslib"),K0={hooks:{afterAllInstalled:async(t,e)=>{await Vc({immutable:e.immutable},...t.workspaces.map(r=>Yc.npath.fromPortablePath(r.cwd)))}}},W0=K0;return wl(V0);})(); +/*! Bundled license information: + +repeat-string/index.js: + (*! + * repeat-string + * + * Copyright (c) 2014-2015, Jon Schlinkert. + * Licensed under the MIT License. + *) + +is-extglob/index.js: + (*! + * is-extglob + * + * Copyright (c) 2014-2016, Jon Schlinkert. + * Licensed under the MIT License. + *) + +is-glob/index.js: + (*! + * is-glob + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + *) + +is-number/index.js: + (*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + *) + +to-regex-range/index.js: + (*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + *) + +fill-range/index.js: + (*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + *) + +queue-microtask/index.js: + (*! queue-microtask. MIT License. Feross Aboukhadijeh *) + +run-parallel/index.js: + (*! run-parallel. MIT License. Feross Aboukhadijeh *) +*/ +return plugin; +} +}; diff --git a/webapp/common-react/.yarnrc.yml b/webapp/common-react/.yarnrc.yml new file mode 100644 index 0000000000..feebba3a65 --- /dev/null +++ b/webapp/common-react/.yarnrc.yml @@ -0,0 +1,7 @@ +enableTelemetry: false +preferReuse: true + +plugins: + - checksum: 50414773926286d8c0fc32e9936a799ad23296281046f03674bc33517b597e1ff7856092c3fcf8d5460a29549ef8b3dca3f59a1732b650a02bf523b255d726e7 + path: .yarn/plugins/@yarnpkg/plugin-ts-project-linker.cjs + spec: "https://raw.githubusercontent.com/Wroud/foundation/refs/heads/main/packages/yarn-plugin-ts-project-linker/bundles/%40yarnpkg/plugin-ts-project-linker.js" diff --git a/webapp/common-react/@dbeaver/react-data-grid/README.md b/webapp/common-react/@dbeaver/react-data-grid/README.md new file mode 100644 index 0000000000..99aa541366 --- /dev/null +++ b/webapp/common-react/@dbeaver/react-data-grid/README.md @@ -0,0 +1 @@ +# @dbeaver/react-data-grid diff --git a/webapp/common-react/@dbeaver/react-data-grid/artifacts/.gitignore b/webapp/common-react/@dbeaver/react-data-grid/artifacts/.gitignore new file mode 100644 index 0000000000..ebf4281dc0 --- /dev/null +++ b/webapp/common-react/@dbeaver/react-data-grid/artifacts/.gitignore @@ -0,0 +1 @@ +!lib diff --git a/webapp/common-react/@dbeaver/react-data-grid/artifacts/lib/index.d.ts b/webapp/common-react/@dbeaver/react-data-grid/artifacts/lib/index.d.ts new file mode 100644 index 0000000000..97cd21826b --- /dev/null +++ b/webapp/common-react/@dbeaver/react-data-grid/artifacts/lib/index.d.ts @@ -0,0 +1,537 @@ +import * as react2 from "react"; +import * as react13 from "react"; +import * as react10 from "react"; +import { Key, ReactElement, ReactNode } from "react"; +import * as react_jsx_runtime1 from "react/jsx-runtime"; +import * as react_jsx_runtime0 from "react/jsx-runtime"; +import * as react_jsx_runtime9 from "react/jsx-runtime"; +import * as react_jsx_runtime11 from "react/jsx-runtime"; +import * as react_jsx_runtime4 from "react/jsx-runtime"; +import * as react_jsx_runtime5 from "react/jsx-runtime"; +import * as react_jsx_runtime7 from "react/jsx-runtime"; + +//#region src/types.d.ts +type Omit = Pick>; +type Maybe = T | undefined | null; +interface Column { + /** The name of the column. Displayed in the header cell by default */ + readonly name: string | ReactElement; + /** A unique key to distinguish each column */ + readonly key: string; + /** + * Column width. If not specified, it will be determined automatically based on grid width and specified widths of other columns + * @default 'auto' + */ + readonly width?: Maybe; + /** + * Minimum column width in pixels + * @default 50 + */ + readonly minWidth?: Maybe; + /** Maximum column width in pixels */ + readonly maxWidth?: Maybe; + /** Class name(s) for the cell */ + readonly cellClass?: Maybe Maybe)>; + /** Class name(s) for the header cell */ + readonly headerCellClass?: Maybe; + /** Class name(s) for the summary cell */ + readonly summaryCellClass?: Maybe Maybe)>; + /** Render function to render the content of cells */ + readonly renderCell?: Maybe<(props: RenderCellProps) => ReactNode>; + /** Render function to render the content of the header cell */ + readonly renderHeaderCell?: Maybe<(props: RenderHeaderCellProps) => ReactNode>; + /** Render function to render the content of summary cells */ + readonly renderSummaryCell?: Maybe<(props: RenderSummaryCellProps) => ReactNode>; + /** Render function to render the content of group cells */ + readonly renderGroupCell?: Maybe<(props: RenderGroupCellProps) => ReactNode>; + /** Render function to render the content of edit cells. When set, the column is automatically set to be editable */ + readonly renderEditCell?: Maybe<(props: RenderEditCellProps) => ReactNode>; + /** Enables cell editing. If set and no editor property specified, then a textinput will be used as the cell editor */ + readonly editable?: Maybe boolean)>; + readonly colSpan?: Maybe<(args: ColSpanArgs) => Maybe>; + /** Determines whether column is frozen */ + readonly frozen?: Maybe; + /** Enable resizing of the column */ + readonly resizable?: Maybe; + /** Enable sorting of the column */ + readonly sortable?: Maybe; + /** Enable dragging of the column */ + readonly draggable?: Maybe; + /** Sets the column sort order to be descending instead of ascending the first time the column is sorted */ + readonly sortDescendingFirst?: Maybe; + /** Options for cell editing */ + readonly editorOptions?: Maybe<{ + /** + * Render the cell content in addition to the edit cell. + * Enable this option when the editor is rendered outside the grid, like a modal for example. + * By default, the cell content is not rendered when the edit cell is open. + * @default false + */ + readonly displayCellContent?: Maybe; + /** + * Commit changes when clicking outside the cell + * @default true + */ + readonly commitOnOutsideClick?: Maybe; + /** + * Close the editor when the row changes externally + * @default true + */ + readonly closeOnExternalRowChange?: Maybe; + }>; +} +interface CalculatedColumn extends Column { + readonly parent: CalculatedColumnParent | undefined; + readonly idx: number; + readonly level: number; + readonly width: number | string; + readonly minWidth: number; + readonly maxWidth: number | undefined; + readonly resizable: boolean; + readonly sortable: boolean; + readonly draggable: boolean; + readonly frozen: boolean; + readonly renderCell: (props: RenderCellProps) => ReactNode; + readonly renderHeaderCell: (props: RenderHeaderCellProps) => ReactNode; +} +interface ColumnGroup { + /** The name of the column group, it will be displayed in the header cell */ + readonly name: string | ReactElement; + readonly headerCellClass?: Maybe; + readonly children: readonly ColumnOrColumnGroup[]; +} +interface CalculatedColumnParent { + readonly name: string | ReactElement; + readonly parent: CalculatedColumnParent | undefined; + readonly idx: number; + readonly colSpan: number; + readonly level: number; + readonly headerCellClass?: Maybe; +} +type ColumnOrColumnGroup = Column | ColumnGroup; +type CalculatedColumnOrColumnGroup = CalculatedColumnParent | CalculatedColumn; +interface Position { + readonly idx: number; + readonly rowIdx: number; +} +interface RenderCellProps { + column: CalculatedColumn; + row: TRow; + rowIdx: number; + isCellEditable: boolean; + tabIndex: number; + onRowChange: (row: TRow) => void; +} +interface RenderSummaryCellProps { + column: CalculatedColumn; + row: TSummaryRow; + tabIndex: number; +} +interface RenderGroupCellProps { + groupKey: unknown; + column: CalculatedColumn; + row: GroupRow; + childRows: readonly TRow[]; + isExpanded: boolean; + tabIndex: number; + toggleGroup: () => void; +} +interface RenderEditCellProps { + column: CalculatedColumn; + row: TRow; + rowIdx: number; + onRowChange: (row: TRow, commitChanges?: boolean) => void; + onClose: (commitChanges?: boolean, shouldFocusCell?: boolean) => void; +} +interface RenderHeaderCellProps { + column: CalculatedColumn; + sortDirection: SortDirection | undefined; + priority: number | undefined; + tabIndex: number; +} +interface BaseCellRendererProps extends Omit, 'children'>, Pick, 'onCellMouseDown' | 'onCellClick' | 'onCellDoubleClick' | 'onCellContextMenu'> { + rowIdx: number; + selectCell: (position: Position, options?: SelectCellOptions) => void; +} +interface CellRendererProps extends BaseCellRendererProps { + column: CalculatedColumn; + row: TRow; + colSpan: number | undefined; + isDraggedOver: boolean; + isCellSelected: boolean; + onRowChange: (column: CalculatedColumn, newRow: TRow) => void; +} +type CellEvent> = E & { + preventGridDefault: () => void; + isGridDefaultPrevented: () => boolean; +}; +type CellMouseEvent = CellEvent>; +type CellKeyboardEvent = CellEvent>; +type CellClipboardEvent = React.ClipboardEvent; +interface CellMouseArgs { + column: CalculatedColumn; + row: TRow; + rowIdx: number; + selectCell: (enableEditor?: boolean) => void; +} +interface SelectCellKeyDownArgs { + mode: 'SELECT'; + column: CalculatedColumn; + row: TRow; + rowIdx: number; + selectCell: (position: Position, options?: SelectCellOptions) => void; +} +interface EditCellKeyDownArgs { + mode: 'EDIT'; + column: CalculatedColumn; + row: TRow; + rowIdx: number; + navigate: () => void; + onClose: (commitChanges?: boolean, shouldFocusCell?: boolean) => void; +} +type CellKeyDownArgs = SelectCellKeyDownArgs | EditCellKeyDownArgs; +interface CellSelectArgs { + rowIdx: number; + row: TRow | undefined; + column: CalculatedColumn; +} +type CellMouseEventHandler = Maybe<(args: CellMouseArgs, NoInfer>, event: CellMouseEvent) => void>; +interface BaseRenderRowProps extends BaseCellRendererProps { + viewportColumns: readonly CalculatedColumn[]; + rowIdx: number; + selectedCellIdx: number | undefined; + isRowSelectionDisabled: boolean; + isRowSelected: boolean; + gridRowStart: number; +} +interface RenderRowProps extends BaseRenderRowProps { + row: TRow; + lastFrozenColumnIndex: number; + draggedOverCellIdx: number | undefined; + selectedCellEditor: ReactElement> | undefined; + onRowChange: (column: CalculatedColumn, rowIdx: number, newRow: TRow) => void; + rowClass: Maybe<(row: TRow, rowIdx: number) => Maybe>; +} +interface RowsChangeData { + indexes: number[]; + column: CalculatedColumn; +} +interface SelectRowEvent { + row: TRow; + checked: boolean; + isShiftClick: boolean; +} +interface SelectHeaderRowEvent { + checked: boolean; +} +interface FillEvent { + columnKey: string; + sourceRow: TRow; + targetRow: TRow; +} +interface CellCopyPasteArgs { + column: CalculatedColumn; + row: TRow; +} +type CellCopyArgs = CellCopyPasteArgs; +type CellPasteArgs = CellCopyPasteArgs; +interface GroupRow { + readonly childRows: readonly TRow[]; + readonly id: string; + readonly parentId: unknown; + readonly groupKey: unknown; + readonly isExpanded: boolean; + readonly level: number; + readonly posInSet: number; + readonly setSize: number; + readonly startRowIndex: number; +} +interface SortColumn { + readonly columnKey: string; + readonly direction: SortDirection; +} +type SortDirection = 'ASC' | 'DESC'; +type ColSpanArgs = { + type: 'HEADER'; +} | { + type: 'ROW'; + row: TRow; +} | { + type: 'SUMMARY'; + row: TSummaryRow; +}; +type RowHeightArgs = { + type: 'ROW'; + row: TRow; +} | { + type: 'GROUP'; + row: GroupRow; +}; +interface RenderSortIconProps { + sortDirection: SortDirection | undefined; +} +interface RenderSortPriorityProps { + priority: number | undefined; +} +interface RenderSortStatusProps extends RenderSortIconProps, RenderSortPriorityProps {} +interface RenderCheckboxProps extends Pick, 'aria-label' | 'aria-labelledby' | 'checked' | 'tabIndex' | 'disabled'> { + indeterminate?: boolean | undefined; + onChange: (checked: boolean, shift: boolean) => void; +} +interface Renderers { + renderCell?: Maybe<(key: Key, props: CellRendererProps) => ReactNode>; + renderCheckbox?: Maybe<(props: RenderCheckboxProps) => ReactNode>; + renderRow?: Maybe<(key: Key, props: RenderRowProps) => ReactNode>; + renderSortStatus?: Maybe<(props: RenderSortStatusProps) => ReactNode>; + noRowsFallback?: Maybe; +} +interface SelectCellOptions { + enableEditor?: Maybe; + shouldFocusCell?: Maybe; +} +interface ColumnWidth { + readonly type: 'resized' | 'measured'; + readonly width: number; +} +type ColumnWidths = ReadonlyMap; +type Direction = 'ltr' | 'rtl'; +//#endregion +//#region src/ScrollToCell.d.ts +interface PartialPosition { + readonly idx?: number | undefined; + readonly rowIdx?: number | undefined; +} +//#endregion +//#region src/DataGrid.d.ts +type DefaultColumnOptions = Pick, 'renderCell' | 'renderHeaderCell' | 'width' | 'minWidth' | 'maxWidth' | 'resizable' | 'sortable' | 'draggable'>; +interface DataGridHandle { + element: HTMLDivElement | null; + scrollToCell: (position: PartialPosition) => void; + selectCell: (position: Position, options?: SelectCellOptions) => void; +} +type SharedDivProps = Pick, 'role' | 'aria-label' | 'aria-labelledby' | 'aria-description' | 'aria-describedby' | 'aria-rowcount' | 'className' | 'style'>; +interface DataGridProps extends SharedDivProps { + ref?: Maybe>; + /** + * Grid and data Props + */ + /** An array of column definitions */ + columns: readonly ColumnOrColumnGroup, NoInfer>[]; + /** A function called for each rendered row that should return a plain key/value pair object */ + rows: readonly R[]; + /** Rows pinned at the top of the grid for summary purposes */ + topSummaryRows?: Maybe; + /** Rows pinned at the bottom of the grid for summary purposes */ + bottomSummaryRows?: Maybe; + /** Function to return a unique key/identifier for each row */ + rowKeyGetter?: Maybe<(row: NoInfer) => K>; + /** Callback triggered when rows are changed */ + onRowsChange?: Maybe<(rows: NoInfer[], data: RowsChangeData, NoInfer>) => void>; + /** + * Dimensions props + */ + /** + * Height of each row in pixels + * @default 35 + */ + rowHeight?: Maybe) => number)>; + /** + * Height of the header row in pixels + * @default 35 + */ + headerRowHeight?: Maybe; + /** + * Height of each summary row in pixels + * @default 35 + */ + summaryRowHeight?: Maybe; + /** A map of column widths */ + columnWidths?: Maybe; + /** Callback triggered when column widths change */ + onColumnWidthsChange?: Maybe<(columnWidths: ColumnWidths) => void>; + /** + * Feature props + */ + /** A set of selected row keys */ + selectedRows?: Maybe>; + /** Function to determine if row selection is disabled for a specific row */ + isRowSelectionDisabled?: Maybe<(row: NoInfer) => boolean>; + /** Callback triggered when the selection changes */ + onSelectedRowsChange?: Maybe<(selectedRows: Set>) => void>; + /** An array of sorted columns */ + sortColumns?: Maybe; + /** Callback triggered when sorting changes */ + onSortColumnsChange?: Maybe<(sortColumns: SortColumn[]) => void>; + /** Default options applied to all columns */ + defaultColumnOptions?: Maybe, NoInfer>>; + onFill?: Maybe<(event: FillEvent>) => NoInfer>; + /** + * Event props + */ + /** Callback triggered when a pointer becomes active in a cell */ + onCellMouseDown?: CellMouseEventHandler; + /** Callback triggered when a cell is clicked */ + onCellClick?: CellMouseEventHandler; + /** Callback triggered when a cell is double-clicked */ + onCellDoubleClick?: CellMouseEventHandler; + /** Callback triggered when a cell is right-clicked */ + onCellContextMenu?: CellMouseEventHandler; + /** Callback triggered when a key is pressed in a cell */ + onCellKeyDown?: Maybe<(args: CellKeyDownArgs, NoInfer>, event: CellKeyboardEvent) => void>; + /** Callback triggered when a cell's content is copied */ + onCellCopy?: Maybe<(args: CellCopyArgs, NoInfer>, event: CellClipboardEvent) => void>; + /** Callback triggered when content is pasted into a cell */ + onCellPaste?: Maybe<(args: CellPasteArgs, NoInfer>, event: CellClipboardEvent) => NoInfer>; + /** Function called whenever cell selection is changed */ + onSelectedCellChange?: Maybe<(args: CellSelectArgs, NoInfer>) => void>; + /** Callback triggered when the grid is scrolled */ + onScroll?: Maybe<(event: React.UIEvent) => void>; + /** Callback triggered when column is resized */ + onColumnResize?: Maybe<(column: CalculatedColumn, width: number) => void>; + /** Callback triggered when columns are reordered */ + onColumnsReorder?: Maybe<(sourceColumnKey: string, targetColumnKey: string) => void>; + /** + * Toggles and modes + */ + /** @default true */ + enableVirtualization?: Maybe; + /** + * Miscellaneous + */ + /** Custom renderers for cells, rows, and other components */ + renderers?: Maybe, NoInfer>>; + /** Function to apply custom class names to rows */ + rowClass?: Maybe<(row: NoInfer, rowIdx: number) => Maybe>; + /** Custom class name for the header row */ + headerRowClass?: Maybe; + /** + * Text direction of the grid ('ltr' or 'rtl') + * @default 'ltr' + * */ + direction?: Maybe; + 'data-testid'?: Maybe; + 'data-cy'?: Maybe; +} +/** + * Main API Component to render a data grid of rows and columns + * + * @example + * + * + */ +declare function DataGrid(props: DataGridProps): react_jsx_runtime1.JSX.Element; +//#endregion +//#region src/TreeDataGrid.d.ts +interface TreeDataGridProps extends Omit, 'columns' | 'role' | 'aria-rowcount' | 'rowHeight' | 'onFill' | 'isRowSelectionDisabled'> { + columns: readonly Column, NoInfer>[]; + rowHeight?: Maybe>) => number)>; + groupBy: readonly string[]; + rowGrouper: (rows: readonly NoInfer[], columnKey: string) => Record[]>; + expandedGroupIds: ReadonlySet; + onExpandedGroupIdsChange: (expandedGroupIds: Set) => void; + groupIdGetter?: Maybe<(groupKey: string, parentId?: string) => string>; +} +declare function TreeDataGrid({ + columns: rawColumns, + rows: rawRows, + rowHeight: rawRowHeight, + rowKeyGetter: rawRowKeyGetter, + onCellKeyDown: rawOnCellKeyDown, + onCellCopy: rawOnCellCopy, + onCellPaste: rawOnCellPaste, + onRowsChange, + selectedRows: rawSelectedRows, + onSelectedRowsChange: rawOnSelectedRowsChange, + renderers, + groupBy: rawGroupBy, + rowGrouper, + expandedGroupIds, + onExpandedGroupIdsChange, + groupIdGetter: rawGroupIdGetter, + ...props +}: TreeDataGridProps): react_jsx_runtime0.JSX.Element; +//#endregion +//#region src/DataGridDefaultRenderersContext.d.ts +declare const DataGridDefaultRenderersContext: react2.Context>>; +//#endregion +//#region src/Row.d.ts +declare const RowComponent: (props: RenderRowProps) => React.JSX.Element; +//#endregion +//#region src/Cell.d.ts +declare const CellComponent: (props: CellRendererProps) => React.JSX.Element; +//#endregion +//#region src/Columns.d.ts +declare const SELECT_COLUMN_KEY = "rdg-select-column"; +declare const SelectColumn: Column; +//#endregion +//#region src/cellRenderers/renderCheckbox.d.ts +declare function renderCheckbox({ + onChange, + indeterminate, + ...props +}: RenderCheckboxProps): react_jsx_runtime9.JSX.Element; +//#endregion +//#region src/cellRenderers/renderToggleGroup.d.ts +declare function renderToggleGroup(props: RenderGroupCellProps): react_jsx_runtime11.JSX.Element; +declare function ToggleGroup({ + groupKey, + isExpanded, + tabIndex, + toggleGroup +}: RenderGroupCellProps): react_jsx_runtime11.JSX.Element; +//#endregion +//#region src/cellRenderers/renderValue.d.ts +declare function renderValue(props: RenderCellProps): react13.ReactNode; +//#endregion +//#region src/cellRenderers/SelectCellFormatter.d.ts +type SharedInputProps = Pick; +interface SelectCellFormatterProps extends SharedInputProps { + value: boolean; +} +declare function SelectCellFormatter({ + value, + tabIndex, + indeterminate, + disabled, + onChange, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy +}: SelectCellFormatterProps): react10.ReactNode; +//#endregion +//#region src/editors/textEditor.d.ts +declare function textEditor({ + row, + column, + onRowChange, + onClose +}: RenderEditCellProps): react_jsx_runtime4.JSX.Element; +//#endregion +//#region src/renderHeaderCell.d.ts +declare function renderHeaderCell({ + column, + sortDirection, + priority +}: RenderHeaderCellProps): string | react_jsx_runtime5.JSX.Element; +//#endregion +//#region src/sortStatus.d.ts +declare function renderSortIcon({ + sortDirection +}: RenderSortIconProps): react_jsx_runtime7.JSX.Element | null; +declare function renderSortPriority({ + priority +}: RenderSortPriorityProps): number | undefined; +//#endregion +//#region src/hooks/useRowSelection.d.ts +declare function useRowSelection(): { + isRowSelectionDisabled: boolean; + isRowSelected: boolean; + onRowSelectionChange: (selectRowEvent: SelectRowEvent) => void; +}; +declare function useHeaderRowSelection(): { + isIndeterminate: boolean; + isRowSelected: boolean; + onRowSelectionChange: (selectRowEvent: SelectHeaderRowEvent) => void; +}; +//#endregion +export { CalculatedColumn, CalculatedColumnOrColumnGroup, CalculatedColumnParent, CellComponent as Cell, CellCopyArgs, CellKeyDownArgs, CellKeyboardEvent, CellMouseArgs, CellMouseEvent, CellPasteArgs, CellRendererProps, CellSelectArgs, ColSpanArgs, Column, ColumnGroup, ColumnOrColumnGroup, ColumnWidth, ColumnWidths, DataGrid, DataGridDefaultRenderersContext, DataGridHandle, DataGridProps, DefaultColumnOptions, FillEvent, RenderCellProps, RenderCheckboxProps, RenderEditCellProps, RenderGroupCellProps, RenderHeaderCellProps, RenderRowProps, RenderSortIconProps, RenderSortPriorityProps, RenderSortStatusProps, RenderSummaryCellProps, Renderers, RowComponent as Row, RowHeightArgs, RowsChangeData, SELECT_COLUMN_KEY, SelectCellFormatter, SelectCellOptions, SelectColumn, SelectHeaderRowEvent, SelectRowEvent, SortColumn, SortDirection, ToggleGroup, TreeDataGrid, TreeDataGridProps, renderCheckbox, renderHeaderCell, renderSortIcon, renderSortPriority, renderToggleGroup, renderValue, textEditor, useHeaderRowSelection, useRowSelection }; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/webapp/common-react/@dbeaver/react-data-grid/artifacts/lib/index.js b/webapp/common-react/@dbeaver/react-data-grid/artifacts/lib/index.js new file mode 100644 index 0000000000..d7b26279c5 --- /dev/null +++ b/webapp/common-react/@dbeaver/react-data-grid/artifacts/lib/index.js @@ -0,0 +1,2849 @@ +import { createContext, memo, useCallback, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { flushSync } from "react-dom"; +import clsx from "clsx"; +import { Fragment, jsx, jsxs } from "react/jsx-runtime"; + +//#region src/utils/colSpanUtils.ts +function getColSpan(column, lastFrozenColumnIndex, args) { + const colSpan = typeof column.colSpan === "function" ? column.colSpan(args) : 1; + if (Number.isInteger(colSpan) && colSpan > 1 && (!column.frozen || column.idx + colSpan - 1 <= lastFrozenColumnIndex)) return colSpan; + return void 0; +} + +//#endregion +//#region src/utils/domUtils.ts +function stopPropagation(event) { + event.stopPropagation(); +} +function scrollIntoView(element, behavior = "instant") { + element?.scrollIntoView({ + inline: "nearest", + block: "nearest", + behavior + }); +} + +//#endregion +//#region src/utils/eventUtils.ts +function createCellEvent(event) { + let defaultPrevented = false; + const cellEvent = { + ...event, + preventGridDefault() { + defaultPrevented = true; + }, + isGridDefaultPrevented() { + return defaultPrevented; + } + }; + Object.setPrototypeOf(cellEvent, Object.getPrototypeOf(event)); + return cellEvent; +} + +//#endregion +//#region src/utils/keyboardUtils.ts +const nonInputKeys = new Set([ + "Unidentified", + "Alt", + "AltGraph", + "CapsLock", + "Control", + "Fn", + "FnLock", + "Meta", + "NumLock", + "ScrollLock", + "Shift", + "Tab", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "End", + "Home", + "PageDown", + "PageUp", + "Insert", + "ContextMenu", + "Escape", + "Pause", + "Play", + "PrintScreen", + "F1", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" +]); +function isCtrlKeyHeldDown(e) { + return (e.ctrlKey || e.metaKey) && e.key !== "Control"; +} +const vKey = 86; +function isDefaultCellInput(event, isUserHandlingPaste) { + if (isCtrlKeyHeldDown(event) && (event.keyCode !== vKey || isUserHandlingPaste)) return false; + return !nonInputKeys.has(event.key); +} +/** +* By default, the following navigation keys are enabled while an editor is open, under specific conditions: +* - Tab: +* - The editor must be an , a (v === 0 ? 60000 : v ?? 1800000) / 1000 / 60} - mapValue={v => (v === undefined ? 30 : Number(v) || 1) * 1000 * 60} + min={MIN_SESSION_EXPIRE_TIME} + mapState={(v: number | undefined) => String((v === 0 ? 60000 : (v ?? 1800000)) / 1000 / 60)} + mapValue={(v?: string) => (v === undefined ? 30 : Number(v) || 1) * 1000 * 60} required tiny > {translate('administration_configuration_wizard_configuration_server_session_lifetime')} + + {translate('administration_configuration_wizard_configuration_secure_cookies_description')} + +

+ {' '} + {translate('administration_configuration_wizard_configuration_secure_cookies_docs')} +
+ + + } + mod={['primary']} + small + > +
+ {translate('administration_configuration_wizard_configuration_secure_cookies')} + {!state.serverConfig.forceHttps && ( + + )} +
+ ); }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx index 7e1e0210f2..0bc0103576 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -11,7 +11,7 @@ import { useCallback } from 'react'; import { Switch, useTranslate } from '@cloudbeaver/core-blocks'; import { CONNECTION_NAVIGATOR_VIEW_SETTINGS, isNavigatorViewSettingsEqual } from '@cloudbeaver/core-root'; -import type { IServerConfigurationPageState } from '../IServerConfigurationPageState'; +import type { IServerConfigurationPageState } from '../IServerConfigurationPageState.js'; interface Props { configs: IServerConfigurationPageState; diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationSecurityForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationSecurityForm.tsx index 1b436642aa..666e5d1ae4 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationSecurityForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationSecurityForm.tsx @@ -1,45 +1,48 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { Group, GroupTitle, Switch, useTranslate } from '@cloudbeaver/core-blocks'; -import type { ServerConfigInput } from '@cloudbeaver/core-sdk'; +import { Group, GroupTitle, Placeholder, Switch, useTranslate, type PlaceholderComponent } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { ServerConfigurationService, type IConfigurationPlaceholderProps } from '../ServerConfigurationService.js'; -interface Props { - serverConfig: ServerConfigInput; -} +export const ServerConfigurationSecurityForm: PlaceholderComponent = observer( + function ServerConfigurationSecurityForm({ state, configurationWizard }) { + const translate = useTranslate(); + const serverConfig = state.serverConfig; + const serverConfigurationService = useService(ServerConfigurationService); -export const ServerConfigurationSecurityForm = observer(function ServerConfigurationSecurityForm({ serverConfig }) { - const translate = useTranslate(); - return ( - - {translate('administration_configuration_wizard_configuration_security')} - - {translate('administration_configuration_wizard_configuration_security_admin_credentials')} - - - {translate('administration_configuration_wizard_configuration_security_public_credentials')} - - - ); -}); + return ( + + {translate('administration_configuration_wizard_configuration_security')} + + {translate('administration_configuration_wizard_configuration_security_admin_credentials')} + + + {translate('administration_configuration_wizard_configuration_security_public_credentials')} + + + + ); + }, +); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/IServerConfigurationFormPartState.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/IServerConfigurationFormPartState.ts new file mode 100644 index 0000000000..bcfaa6d7db --- /dev/null +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/IServerConfigurationFormPartState.ts @@ -0,0 +1,49 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { schema } from '@cloudbeaver/core-utils'; + +const ServerConfigurationFormPartStateConfigSchema = schema.object({ + adminCredentialsSaveEnabled: schema.boolean().optional(), + adminName: schema.string().trim().optional(), + adminPassword: schema.string().trim().optional(), + adminPasswordRepeat: schema.string().trim().optional(), + anonymousAccessEnabled: schema.boolean().optional(), + authenticationEnabled: schema.boolean().optional(), + customConnectionsEnabled: schema.boolean().optional(), + disabledDrivers: schema.array(schema.string()).optional(), + enabledAuthProviders: schema.array(schema.string()).optional(), + enabledFeatures: schema.array(schema.string()).optional(), + publicCredentialsSaveEnabled: schema.boolean().optional(), + resourceManagerEnabled: schema.boolean().optional(), + secretManagerEnabled: schema.boolean().optional(), + serverName: schema.string().trim().optional(), + serverURL: schema.string().trim().optional(), + sessionExpireTime: schema.number().optional(), + forceHttps: schema.boolean().optional(), + supportedHosts: schema.string(), + bindSessionToIp: schema.string().optional(), +}); + +const ServerConfigurationFormPartStateNavigatorSchema = schema.object({ + hideFolders: schema.boolean(), + hideSchemas: schema.boolean(), + hideVirtualModel: schema.boolean(), + mergeEntities: schema.boolean(), + showOnlyEntities: schema.boolean(), + showSystemObjects: schema.boolean(), + showUtilityObjects: schema.boolean(), +}); + +export const ServerConfigStateSchema = schema.object({ + serverConfig: ServerConfigurationFormPartStateConfigSchema, + navigatorConfig: ServerConfigurationFormPartStateNavigatorSchema, +}); + +export type IServerConfig = schema.infer; +export type INavigatorConfig = schema.infer; +export type IServerConfigurationFormPartState = schema.infer; diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/IServerConfigurationPageState.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/IServerConfigurationPageState.ts index bcc79020f2..69b1586739 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/IServerConfigurationPageState.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/IServerConfigurationPageState.ts @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { NavigatorSettingsInput, ServerConfigInput } from '@cloudbeaver/core-sdk'; +import type { INavigatorConfig, IServerConfig } from './IServerConfigurationFormPartState.js'; export interface IServerConfigurationPageState { - serverConfig: ServerConfigInput; - navigatorConfig: NavigatorSettingsInput; + serverConfig: IServerConfig; + navigatorConfig: INavigatorConfig; } diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDrawerItem.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDrawerItem.tsx index 8d3e73e05b..0e83e06bb2 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDrawerItem.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDrawerItem.tsx @@ -1,28 +1,25 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import styled from 'reshadow'; - import type { AdministrationItemDrawerProps } from '@cloudbeaver/core-administration'; -import { Translate, useStyles } from '@cloudbeaver/core-blocks'; -import { Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; +import { Translate } from '@cloudbeaver/core-blocks'; +import { TabIcon, Tab, TabTitle } from '@cloudbeaver/core-ui'; export const ServerConfigurationDrawerItem: React.FC = function ServerConfigurationDrawerItem({ item, onSelect, - style, disabled, }) { - return styled(useStyles(style))( + return ( onSelect(item.name)}> - + - , + ); }; diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.m.css b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.m.css deleted file mode 100644 index 7504377d21..0000000000 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.m.css +++ /dev/null @@ -1,4 +0,0 @@ -.wrapper { - max-height: 100px; - overflow: auto; -} \ No newline at end of file diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.module.css b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.module.css new file mode 100644 index 0000000000..ea65252fc3 --- /dev/null +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.wrapper { + max-height: 100px; + overflow: auto; +} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx index d4e3f7ba97..2d091b2c95 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx @@ -1,31 +1,35 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useCallback } from 'react'; -import styled from 'reshadow'; -import { Combobox, Group, GroupTitle, ITag, s, Tag, Tags, useResource, useS, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { Combobox, ConfirmationDialog, Group, GroupTitle, type ITag, s, Tag, Tags, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; import { DBDriverResource } from '@cloudbeaver/core-connections'; import { CachedMapAllKey, resourceKeyList } from '@cloudbeaver/core-resource'; -import type { ServerConfigInput } from '@cloudbeaver/core-sdk'; +import { isDefined } from '@dbeaver/js-helpers'; -import style from './ServerConfigurationDriversForm.m.css'; +import style from './ServerConfigurationDriversForm.module.css'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import type { IServerConfig } from './IServerConfigurationFormPartState.js'; interface Props { - serverConfig: ServerConfigInput; + serverConfig: IServerConfig; + initialServerConfig: IServerConfig; } -export const ServerConfigurationDriversForm = observer(function ServerConfigurationDriversForm({ serverConfig }) { +export const ServerConfigurationDriversForm = observer(function ServerConfigurationDriversForm({ serverConfig, initialServerConfig }) { const styles = useS(style); 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 commonDialogService = useService(CommonDialogService); const tags: ITag[] = driversResource.resource .get(resourceKeyList(serverConfig.disabledDrivers || [])) @@ -36,25 +40,46 @@ export const ServerConfigurationDriversForm = observer(function ServerCon icon: driver!.icon, })); - const handleSelect = useCallback((value: string) => { - if (serverConfig.disabledDrivers && !serverConfig.disabledDrivers.includes(value)) { - serverConfig.disabledDrivers.push(value); - } - }, []); + const handleSelect = useCallback( + (value: string) => { + if (serverConfig.disabledDrivers && !serverConfig.disabledDrivers.includes(value)) { + serverConfig.disabledDrivers.push(value); + } + }, + [serverConfig.disabledDrivers], + ); - const handleRemove = useCallback((id: string) => { + async function handleRemove(id: string) { if (!serverConfig.disabledDrivers) { return; } + const driver = driversResource.resource.get(id); + const isInitiallyDisabledDriver = initialServerConfig.disabledDrivers?.includes(id); + + if (driver?.embedded && !driver?.safeEmbeddedDriver && isInitiallyDisabledDriver) { + const result = await commonDialogService.open(ConfirmationDialog, { + title: 'ui_security_warning', + message: translate('administration_disabled_drivers_enable_unsafe_driver_message', undefined, { driverName: driver?.name || id }), + confirmActionText: 'ui_enable', + icon: '/icons/warning_icon.svg', + bigIcon: true, + size: 'medium', + }); + + if (result === DialogueStateResult.Rejected) { + return; + } + } + const index = serverConfig.disabledDrivers.indexOf(id); if (index !== -1) { serverConfig.disabledDrivers.splice(index, 1); } - }, []); + } - return styled(useStyles(style))( + return ( {translate('administration_disabled_drivers_title')} (function ServerCon isDisabled={item => serverConfig.disabledDrivers?.includes(item.id) ?? false} items={drivers} placeholder={translate('administration_disabled_drivers_search_placeholder')} - searchable onSelect={handleSelect} /> @@ -72,6 +96,6 @@ export const ServerConfigurationDriversForm = observer(function ServerCon ))} - , + ); }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormPart.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormPart.ts new file mode 100644 index 0000000000..f16b23de7c --- /dev/null +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormPart.ts @@ -0,0 +1,172 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { AdministrationScreenService } from '@cloudbeaver/core-administration'; +import { ADMIN_USERNAME_MIN_LENGTH, AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, PasswordPolicyService } from '@cloudbeaver/core-authentication'; +import { DEFAULT_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections'; +import { ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { DefaultNavigatorSettingsResource, PasswordPolicyResource, ProductInfoResource, ServerConfigResource } from '@cloudbeaver/core-root'; +import { FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui'; +import { isIp, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +import { MIN_SESSION_EXPIRE_TIME } from './Form/MIN_SESSION_EXPIRE_TIME.js'; +import { ServerConfigStateSchema, type IServerConfigurationFormPartState } from './IServerConfigurationFormPartState.js'; + +function DEFAULT_STATE_GETTER(): IServerConfigurationFormPartState { + return { + serverConfig: { + adminCredentialsSaveEnabled: false, + anonymousAccessEnabled: false, + authenticationEnabled: false, + customConnectionsEnabled: false, + disabledDrivers: [], + enabledAuthProviders: [], + enabledFeatures: [], + publicCredentialsSaveEnabled: false, + resourceManagerEnabled: false, + secretManagerEnabled: false, + serverName: '', + serverURL: '', + sessionExpireTime: MIN_SESSION_EXPIRE_TIME * 1000 * 60, + forceHttps: true, + supportedHosts: '', + }, + navigatorConfig: { ...DEFAULT_NAVIGATOR_VIEW_SETTINGS }, + }; +} + +const SUPPORTED_HOSTS_SPLITTER = '\n'; +export class ServerConfigurationFormPart extends FormPart { + constructor( + formState: IFormState, + private readonly administrationScreenService: AdministrationScreenService, + private readonly serverConfigResource: ServerConfigResource, + private readonly productInfoResource: ProductInfoResource, + private readonly defaultNavigatorSettingsResource: DefaultNavigatorSettingsResource, + private readonly authProvidersResource: AuthProvidersResource, + private readonly passwordPolicyResource: PasswordPolicyResource, + private readonly passwordPolicyService: PasswordPolicyService, + private readonly localizationService: LocalizationService, + ) { + super(formState, DEFAULT_STATE_GETTER(), ServerConfigStateSchema); + } + + override isOutdated(): boolean { + return super.isOutdated() || this.serverConfigResource.isOutdated() || this.defaultNavigatorSettingsResource.isOutdated(); + } + + override isLoaded(): boolean { + return super.isLoaded() && this.serverConfigResource.isLoaded() && this.defaultNavigatorSettingsResource.isLoaded(); + } + + protected override async validate( + data: IFormState, + contexts: IExecutionContextProvider>, + ) { + const validation = contexts.getContext(formValidationContext); + + const supportedHosts = this.state.serverConfig.supportedHosts; + const currentHost = window.location.host; + + if (!isIp(window.location.hostname) && supportedHosts.trim() && !supportedHosts.includes(currentHost)) { + validation.error( + this.localizationService.translate('administration_configuration_wizard_configuration_supported_hosts_warning', undefined, { + host: currentHost, + }), + ); + } + + if (this.administrationScreenService.isConfigurationMode) { + await this.authProvidersResource.load(CachedMapAllKey); + + if (this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID)) { + await this.passwordPolicyResource.load(); + + const isNameValid = this.state.serverConfig.adminName && this.state.serverConfig.adminName.length >= ADMIN_USERNAME_MIN_LENGTH; + const isPasswordValid = this.passwordPolicyService.validatePassword(this.state.serverConfig.adminPassword ?? ''); + const isPasswordRepeated = isValuesEqual(this.state.serverConfig.adminPassword, this.state.serverConfig.adminPasswordRepeat, null); + + if (!isNameValid || !isPasswordValid.isValid || !isPasswordRepeated) { + ExecutorInterrupter.interrupt(contexts); + } + } + } + } + + override get isChanged(): boolean { + if (this.loaded && this.administrationScreenService.isConfigurationMode) { + return true; + } + + return super.isChanged; + } + + protected override async saveChanges() { + if (!isObjectsEqual(this.state.navigatorConfig, this.initialState.navigatorConfig)) { + await this.defaultNavigatorSettingsResource.save(this.state.navigatorConfig); + } + + // Exclude adminPasswordRepeat from server payload as it's only for client-side validation + const { adminPasswordRepeat, ...serverConfigToSave } = this.state.serverConfig; + await this.serverConfigResource.save({ + ...serverConfigToSave, + supportedHosts: Array.from( + new Set( + this.state.serverConfig.supportedHosts + .split(SUPPORTED_HOSTS_SPLITTER) + .map(host => host.trim()) + .filter(Boolean), + ), + ), + }); + } + + protected override async loader() { + const [config, productInfo, defaultNavigatorSettings] = await Promise.all([ + this.serverConfigResource.load(), + this.productInfoResource.load(), + this.defaultNavigatorSettingsResource.load(), + ]); + + let adminName: string | undefined; + let adminPassword: string | undefined; + + if (this.administrationScreenService.isConfigurationMode) { + await this.authProvidersResource.load(CachedMapAllKey); + + if (this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID)) { + adminName = 'cbadmin'; + adminPassword = ''; + } + } + + this.setInitialState({ + serverConfig: { + adminName, + adminPassword, + serverName: config?.name || productInfo?.name, + serverURL: this.administrationScreenService.isConfigurationMode && !config?.distributed ? window.location.origin : (config?.serverURL ?? ''), + sessionExpireTime: config?.sessionExpireTime ?? MIN_SESSION_EXPIRE_TIME * 1000 * 60, + adminCredentialsSaveEnabled: config?.adminCredentialsSaveEnabled ?? false, + publicCredentialsSaveEnabled: config?.publicCredentialsSaveEnabled ?? false, + customConnectionsEnabled: config?.supportsCustomConnections ?? false, + disabledDrivers: config?.disabledDrivers ? [...config.disabledDrivers] : [], + enabledAuthProviders: config?.enabledAuthProviders ? [...config.enabledAuthProviders] : [], + anonymousAccessEnabled: config?.anonymousAccessEnabled ?? false, + enabledFeatures: config?.enabledFeatures ? [...config.enabledFeatures] : [], + resourceManagerEnabled: config?.resourceManagerEnabled ?? false, + secretManagerEnabled: config?.secretManagerEnabled ?? false, + supportedHosts: config?.supportedHosts.join(SUPPORTED_HOSTS_SPLITTER) ?? '', + forceHttps: config?.forceHttps ?? true, + bindSessionToIp: config?.bindSessionToIp, + }, + navigatorConfig: { ...this.state.navigatorConfig, ...defaultNavigatorSettings }, + }); + } +} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormService.ts new file mode 100644 index 0000000000..c669ab6e5e --- /dev/null +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormService.ts @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { FormBaseService, type IFormProps } from '@cloudbeaver/core-ui'; + +export type ServerConfigurationFormProps = IFormProps; + +@injectable(() => [LocalizationService, NotificationService]) +export class ServerConfigurationFormService extends FormBaseService { + constructor(localizationService: LocalizationService, notificationService: NotificationService) { + super(localizationService, notificationService, 'Server Configuration form'); + } +} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormState.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormState.ts new file mode 100644 index 0000000000..8e97b2b811 --- /dev/null +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormState.ts @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IServiceProvider } from '@cloudbeaver/core-di'; +import { FormState } from '@cloudbeaver/core-ui'; + +import type { ServerConfigurationFormService } from './ServerConfigurationFormService.js'; + +export class ServerConfigurationFormState extends FormState { + constructor(serviceProvider: IServiceProvider, service: ServerConfigurationFormService) { + super(serviceProvider, service, null); + } +} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormStateManager.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormStateManager.ts new file mode 100644 index 0000000000..9b0b895aec --- /dev/null +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationFormStateManager.ts @@ -0,0 +1,54 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 } from 'mobx'; + +import { injectable, IServiceProvider } from '@cloudbeaver/core-di'; +import type { IFormState } from '@cloudbeaver/core-ui'; + +import { ServerConfigurationFormService } from './ServerConfigurationFormService.js'; +import { ServerConfigurationFormState } from './ServerConfigurationFormState.js'; + +@injectable(() => [IServiceProvider, ServerConfigurationFormService]) +export class ServerConfigurationFormStateManager { + formState: ServerConfigurationFormState | null; + + constructor( + private readonly serviceProvider: IServiceProvider, + private readonly serverConfigurationFormService: ServerConfigurationFormService, + ) { + this.formState = null; + + makeObservable(this, { + formState: observable.ref, + }); + } + + create(): IFormState { + if (this.formState) { + return this.formState; + } + + this.formState = new ServerConfigurationFormState(this.serviceProvider, this.serverConfigurationFormService); + return this.formState; + } + + async save(): Promise { + if (!this.formState) { + return false; + } + + return await this.formState.save(); + } + + destroy(): void { + if (this.formState) { + this.formState?.dispose(); + this.formState = null; + } + } +} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.module.css b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.module.css new file mode 100644 index 0000000000..0dc8aef50b --- /dev/null +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.module.css @@ -0,0 +1,10 @@ +.message { + white-space: pre-wrap; + line-height: 2; + margin-block-start: 1em; + margin-block-end: 1em; +} + +.loader { + height: 100%; +} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx index 94cd32626a..f8af274989 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx @@ -1,14 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled, { css } from 'reshadow'; -import { ADMINISTRATION_TOOLS_PANEL_STYLES, AdministrationItemContentComponent, ConfigurationWizardService } from '@cloudbeaver/core-administration'; +import { type AdministrationItemContentComponent, ConfigurationWizardService } from '@cloudbeaver/core-administration'; import { ColoredContainer, ConfirmationDialog, @@ -17,143 +16,149 @@ import { Group, GroupItem, GroupTitle, - Loader, Placeholder, + s, ToolsAction, ToolsPanel, + useAutoLoad, useFocus, + useForm, useFormValidator, - useStyles, + useS, useTranslate, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; - -import { ServerConfigurationConfigurationForm } from './Form/ServerConfigurationConfigurationForm'; -import { ServerConfigurationFeaturesForm } from './Form/ServerConfigurationFeaturesForm'; -import { ServerConfigurationInfoForm } from './Form/ServerConfigurationInfoForm'; -import { ServerConfigurationNavigatorViewForm } from './Form/ServerConfigurationNavigatorViewForm'; -import { ServerConfigurationSecurityForm } from './Form/ServerConfigurationSecurityForm'; -import { ServerConfigurationDriversForm } from './ServerConfigurationDriversForm'; -import { ServerConfigurationService } from './ServerConfigurationService'; - -const styles = css` - Form { - flex: 1; - display: flex; - overflow: auto; - flex-direction: column; - } +import { NotificationService } from '@cloudbeaver/core-events'; +import { getFirstException } from '@cloudbeaver/core-utils'; - p { - white-space: pre-wrap; - line-height: 2; - } - - Loader { - height: 100%; - } -`; +import { ServerConfigurationConfigurationForm } from './Form/ServerConfigurationConfigurationForm.js'; +import { ServerConfigurationFeaturesForm } from './Form/ServerConfigurationFeaturesForm.js'; +import { ServerConfigurationInfoForm } from './Form/ServerConfigurationInfoForm.js'; +import { ServerConfigurationNavigatorViewForm } from './Form/ServerConfigurationNavigatorViewForm.js'; +import { ServerConfigurationSecurityForm } from './Form/ServerConfigurationSecurityForm.js'; +import { getServerConfigurationFormPart } from './getServerConfigurationFormPart.js'; +import { ServerConfigurationDriversForm } from './ServerConfigurationDriversForm.js'; +import { ServerConfigurationFormStateManager } from './ServerConfigurationFormStateManager.js'; +import style from './ServerConfigurationPage.module.css'; +import { ServerConfigurationService } from './ServerConfigurationService.js'; export const ServerConfigurationPage: AdministrationItemContentComponent = observer(function ServerConfigurationPage({ configurationWizard }) { const translate = useTranslate(); - const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES); - const [focusedRef, state] = useFocus({ focusFirstChild: true }); - const service = useService(ServerConfigurationService); - const serverConfigResource = useService(ServerConfigResource); + const styles = useS(style); + const [focusedRef, ref] = useFocus({ focusFirstChild: true }); + const serverConfigurationService = useService(ServerConfigurationService); const commonDialogService = useService(CommonDialogService); + const notificationService = useService(NotificationService); + const serverConfigurationFormStateManager = useService(ServerConfigurationFormStateManager); const configurationWizardService = useService(ConfigurationWizardService); - const changed = serverConfigResource.isChanged() || serverConfigResource.isNavigatorSettingsChanged(); - useFormValidator(service.validationTask, state.reference); + + const formState = serverConfigurationFormStateManager.formState!; + const part = getServerConfigurationFormPart(formState); + + useAutoLoad(ServerConfigurationPage, [part]); + useFormValidator(formState.validationTask, ref.reference); function handleChange() { - service.changed(); + if (configurationWizard) { + serverConfigurationService.setDone(false); + } - if (!service.state.serverConfig.adminCredentialsSaveEnabled) { - service.state.serverConfig.publicCredentialsSaveEnabled = false; + if (!part.state.serverConfig.adminCredentialsSaveEnabled) { + part.state.serverConfig.publicCredentialsSaveEnabled = false; } } - function reset() { - service.loadConfig(true); - } + const changed = part.isChanged; async function save() { if (configurationWizard) { - await configurationWizardService.next(); - } else { - if (serverConfigResource.isChanged()) { - const result = await commonDialogService.open(ConfirmationDialog, { - title: 'administration_server_configuration_save_confirmation_title', - message: 'administration_server_configuration_save_confirmation_message', - }); - - if (result === DialogueStateResult.Rejected) { - return; - } + configurationWizardService.next(); + return; + } + + if (changed) { + const result = await commonDialogService.open(ConfirmationDialog, { + title: 'administration_server_configuration_save_confirmation_title', + message: 'administration_server_configuration_save_confirmation_message', + }); + + if (result === DialogueStateResult.Rejected) { + return; } - await service.saveConfiguration(true); + } + + const saved = await serverConfigurationFormStateManager.save(); + + if (!saved) { + const error = getFirstException(part.exception); + if (error) { + notificationService.logException(error, 'administration_configuration_wizard_configuration_save_error'); + return; + } + + notificationService.logError({ title: 'administration_configuration_wizard_configuration_save_error' }); } } - return styled(style)( -
+ const form = useForm({ + onSubmit: save, + }); + + return ( + {!configurationWizard && ( - - - {translate('ui_processing_save')} - - - {translate('ui_processing_cancel')} - - + + + form.submit()} + > + {translate('ui_processing_save')} + + formState.reset()} + > + {translate('ui_processing_cancel')} + + + )} - + + {configurationWizard && ( -

{translate('administration_configuration_wizard_configuration_title')}

+

{translate('administration_configuration_wizard_configuration_title')}

-

{translate('administration_configuration_wizard_configuration_message')}

+

{translate('administration_configuration_wizard_configuration_message')}

)} - - {() => - styled(style)( - - - - - {translate('administration_configuration_wizard_configuration_plugins')} - - - - - - - - - - , - ) - } - -
- , +
+ + + + {translate('administration_configuration_wizard_configuration_plugins')} + + + + + + + + + +
+ +
); }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts index f28e342395..d7eac9629e 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts @@ -1,321 +1,49 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 } from 'mobx'; -import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { ActionSnackbar, ActionSnackbarProps, PlaceholderContainer } from '@cloudbeaver/core-blocks'; -import { DEFAULT_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections'; +import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; -import { ENotificationType, INotification, NotificationService } from '@cloudbeaver/core-events'; -import { Executor, ExecutorInterrupter, IExecutor, IExecutorHandler } from '@cloudbeaver/core-executor'; -import { ServerConfigResource, SessionDataResource } from '@cloudbeaver/core-root'; +import { formValidationContext } from '@cloudbeaver/core-ui'; -import { ADMINISTRATION_SERVER_CONFIGURATION_ITEM } from './ADMINISTRATION_SERVER_CONFIGURATION_ITEM'; -import type { IServerConfigurationPageState } from './IServerConfigurationPageState'; +import type { IServerConfigurationFormPartState } from './IServerConfigurationFormPartState.js'; +import { ServerConfigurationFormService } from './ServerConfigurationFormService.js'; export interface IConfigurationPlaceholderProps { configurationWizard: boolean; - state: IServerConfigurationPageState; + state: IServerConfigurationFormPartState; } -export interface IServerConfigSaveData { - state: IServerConfigurationPageState; - configurationWizard: boolean; - finish: boolean; -} - -export interface ILoadConfigData { - state: IServerConfigurationPageState; - reset: boolean; -} - -@injectable() +@injectable(() => [ServerConfigurationFormService]) export class ServerConfigurationService { - state: IServerConfigurationPageState; - loading: boolean; + isDone: boolean; - readonly loadConfigTask: IExecutor; - readonly prepareConfigTask: IExecutor; - readonly saveTask: IExecutor; - readonly validationTask: IExecutor; readonly configurationContainer: PlaceholderContainer; readonly pluginsContainer: PlaceholderContainer; + readonly securitySettingsContainer: PlaceholderContainer; - private done: boolean; - private stateLinked: boolean; - private unSaveNotification: INotification | null; - - constructor( - private readonly administrationScreenService: AdministrationScreenService, - private readonly serverConfigResource: ServerConfigResource, - private readonly notificationService: NotificationService, - private readonly sessionDataResource: SessionDataResource, - ) { - this.done = false; - this.loading = true; - this.state = serverConfigStateContext(); - - makeObservable(this, { - state: observable, - loading: observable, - done: observable, - }); - - this.stateLinked = false; - this.unSaveNotification = null; - this.loadConfigTask = new Executor(); - this.prepareConfigTask = new Executor(); - this.saveTask = new Executor(); - this.validationTask = new Executor(); + constructor(private readonly serverConfigurationFormService: ServerConfigurationFormService) { + this.isDone = false; this.configurationContainer = new PlaceholderContainer(); this.pluginsContainer = new PlaceholderContainer(); + this.securitySettingsContainer = new PlaceholderContainer(); - this.loadConfigTask - .next(this.validationTask, () => this.getSaveData(false)) - .addHandler(() => { - this.loading = true; - }) - .addHandler(this.loadServerConfig) - .addPostHandler(() => { - this.loading = false; - this.showUnsavedNotification(false); - }); - - this.saveTask.before(this.validationTask).before(this.prepareConfigTask).addPostHandler(this.save); - - this.validationTask.addHandler(this.validateForm).addPostHandler(this.ensureValidation); - - this.serverConfigResource.onDataUpdate.addPostHandler(this.showUnsavedNotification.bind(this, false)); - - this.administrationScreenService.activationEvent.addHandler(this.unlinkState.bind(this)); - } - - changed(): void { - this.done = false; - - this.showUnsavedNotification(true); - } - - deactivate(configurationWizard: boolean, outside: boolean, outsideAdminPage: boolean): void { - if (!outsideAdminPage) { - this.showUnsavedNotification(false); - } - } - - async activate(): Promise { - // this.unSaveNotification?.close(true); - await this.loadConfig(); - } - - async loadConfig(reset = false): Promise { - try { - if (!this.stateLinked) { - this.state = this.administrationScreenService.getItemState( - 'server-configuration', - () => { - reset = true; - return serverConfigStateContext(); - }, - true, - ); - - this.stateLinked = true; - await this.serverConfigResource.load(); - - this.serverConfigResource.setDataUpdate(this.state.serverConfig); - this.serverConfigResource.setNavigatorSettingsUpdate(this.state.navigatorConfig); - - if (reset) { - this.serverConfigResource.resetUpdate(); - } - } - - await this.loadConfigTask.execute({ - state: this.state, - reset, - }); - } catch (exception: any) { - this.notificationService.logException(exception, "Can't load server configuration"); - } - } - - isDone(): boolean { - return this.done; - } - - async saveConfiguration(finish: boolean): Promise { - const contexts = await this.saveTask.execute(this.getSaveData(finish)); - - const validation = contexts.getContext(serverConfigValidationContext); - - return validation.getState(); - } - - private readonly loadServerConfig: IExecutorHandler = async (data, contexts) => { - if (!data.reset) { - return; - } - - try { - const config = await this.serverConfigResource.load(); - - if (!config) { - return; - } - - data.state.serverConfig.serverName = config.name || config.productInfo.name; - data.state.serverConfig.serverURL = config.serverURL; - - if (this.administrationScreenService.isConfigurationMode && !config.distributed) { - data.state.serverConfig.serverURL = window.location.origin; - } - - data.state.serverConfig.sessionExpireTime = config.sessionExpireTime; - - data.state.serverConfig.adminCredentialsSaveEnabled = config.adminCredentialsSaveEnabled; - data.state.serverConfig.publicCredentialsSaveEnabled = config.publicCredentialsSaveEnabled; - data.state.serverConfig.customConnectionsEnabled = config.supportsCustomConnections; - data.state.serverConfig.disabledDrivers = [...config.disabledDrivers]; - - Object.assign(data.state.navigatorConfig, config.defaultNavigatorSettings); - } catch (exception: any) { - ExecutorInterrupter.interrupt(contexts); - this.notificationService.logException(exception, "Can't load server configuration"); - } - }; - - private getSaveData(finish: boolean): IServerConfigSaveData { - return { - state: this.state, - finish, - configurationWizard: this.administrationScreenService.isConfigurationMode, - }; - } - - private readonly save: IExecutorHandler = async (data, contexts) => { - const validation = contexts.getContext(serverConfigValidationContext); - - if (!validation.getState()) { - return; - } - - try { - await this.serverConfigResource.save(data.configurationWizard); - - if (data.configurationWizard && data.finish) { - await this.serverConfigResource.finishConfiguration(); - await this.sessionDataResource.refresh(); - } - } catch (exception: any) { - this.notificationService.logException(exception, "Can't save server configuration"); - - throw exception; - } - }; - - private readonly ensureValidation: IExecutorHandler = (data, contexts) => { - const validation = contexts.getContext(serverConfigValidationContext); - - if (!validation.getState()) { - ExecutorInterrupter.interrupt(contexts); - this.done = false; - } else { - this.done = true; - } - }; - - private readonly validateForm: IExecutorHandler = (data, contexts) => { - const validation = contexts.getContext(serverConfigValidationContext); - - if (!this.isFormFilled(data.state)) { - validation.invalidate(); - } - }; - - private isFormFilled(state: IServerConfigurationPageState) { - if (!state.serverConfig.serverName) { - return false; - } - if ((state.serverConfig.sessionExpireTime ?? 0) < 1) { - return false; - } - return true; - } - - private showUnsavedNotification(close: boolean) { - if ( - (!this.serverConfigResource.isChanged() && !this.serverConfigResource.isNavigatorSettingsChanged()) || - this.administrationScreenService.activeScreen?.item === ADMINISTRATION_SERVER_CONFIGURATION_ITEM - ) { - this.unSaveNotification?.close(true); - return; - } - - if ( - close || - !this.stateLinked || - this.unSaveNotification || - this.administrationScreenService.isConfigurationMode - // || !this.administrationScreenService.isAdministrationPageActive - ) { - return; - } + this.serverConfigurationFormService.onValidate.addHandler((data, contexts) => { + const validation = contexts.getContext(formValidationContext); + this.setDone(validation.valid); + }); - this.unSaveNotification = this.notificationService.customNotification( - () => ActionSnackbar, - { - actionText: 'administration_configuration_wizard_configuration_server_info_unsaved_navigate', - onAction: () => this.administrationScreenService.navigateToItem(ADMINISTRATION_SERVER_CONFIGURATION_ITEM), - }, - { - title: 'administration_configuration_wizard_configuration_server_info_unsaved_title', - message: 'administration_configuration_wizard_configuration_server_info_unsaved_message', - type: ENotificationType.Info, - onClose: () => { - this.unSaveNotification = null; - }, - }, - ); + makeObservable(this, { + isDone: observable.ref, + }); } - private unlinkState(state: boolean): void { - if (state) { - return; - } - - this.unSaveNotification?.close(true); - this.serverConfigResource.unlinkUpdate(); - this.stateLinked = false; + setDone(value: boolean) { + this.isDone = value; } } - -export interface IValidationStatusContext { - getState: () => boolean; - invalidate: () => void; -} - -export function serverConfigValidationContext(): IValidationStatusContext { - let state = true; - - const invalidate = () => { - state = false; - }; - const getState = () => state; - - return { - getState, - invalidate, - }; -} - -export function serverConfigStateContext(): IServerConfigurationPageState { - return { - navigatorConfig: { ...DEFAULT_NAVIGATOR_VIEW_SETTINGS }, - serverConfig: {}, - }; -} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/getServerConfigurationFormPart.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/getServerConfigurationFormPart.ts new file mode 100644 index 0000000000..d7afcb34ee --- /dev/null +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/getServerConfigurationFormPart.ts @@ -0,0 +1,43 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { AdministrationScreenService } from '@cloudbeaver/core-administration'; +import { AuthProvidersResource, PasswordPolicyService } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import { DefaultNavigatorSettingsResource, PasswordPolicyResource, ProductInfoResource, ServerConfigResource } from '@cloudbeaver/core-root'; +import type { IFormState } from '@cloudbeaver/core-ui'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +import { ServerConfigurationFormPart } from './ServerConfigurationFormPart.js'; + +const DATA_CONTEXT_SERVER_CONFIGURATION_FORM_PART = createDataContext('Server Configuration form Part'); + +export function getServerConfigurationFormPart(formState: IFormState): ServerConfigurationFormPart { + return formState.getPart(DATA_CONTEXT_SERVER_CONFIGURATION_FORM_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const administrationScreenService = di.getService(AdministrationScreenService); + const serverConfigResource = di.getService(ServerConfigResource); + const defaultNavigatorSettingsResource = di.getService(DefaultNavigatorSettingsResource); + const productInfoResource = di.getService(ProductInfoResource); + const authProvidersResource = di.getService(AuthProvidersResource); + const passwordPolicyResource = di.getService(PasswordPolicyResource); + const passwordPolicyService = di.getService(PasswordPolicyService); + const localizationService = di.getService(LocalizationService); + + return new ServerConfigurationFormPart( + formState, + administrationScreenService, + serverConfigResource, + productInfoResource, + defaultNavigatorSettingsResource, + authProvidersResource, + passwordPolicyResource, + passwordPolicyService, + localizationService, + ); + }); +} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfigurationAdministrationNavService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfigurationAdministrationNavService.ts index 9418910986..55b572ea57 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfigurationAdministrationNavService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfigurationAdministrationNavService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,9 +8,9 @@ import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { injectable } from '@cloudbeaver/core-di'; -import { ADMINISTRATION_SERVER_CONFIGURATION_ITEM } from './ServerConfiguration/ADMINISTRATION_SERVER_CONFIGURATION_ITEM'; +import { ADMINISTRATION_SERVER_CONFIGURATION_ITEM } from './ServerConfiguration/ADMINISTRATION_SERVER_CONFIGURATION_ITEM.js'; -@injectable() +@injectable(() => [AdministrationScreenService]) export class ServerConfigurationAdministrationNavService { constructor(private readonly administrationScreenService: AdministrationScreenService) {} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomeDrawerItem.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomeDrawerItem.tsx index c70af18536..d8c647afda 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomeDrawerItem.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomeDrawerItem.tsx @@ -1,23 +1,21 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import styled from 'reshadow'; - import type { AdministrationItemDrawerProps } from '@cloudbeaver/core-administration'; -import { Translate, useStyles } from '@cloudbeaver/core-blocks'; -import { Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; +import { Translate } from '@cloudbeaver/core-blocks'; +import { TabIcon, Tab, TabTitle } from '@cloudbeaver/core-ui'; -export const WelcomeDrawerItem: React.FC = function WelcomeDrawerItem({ item, onSelect, style, disabled }) { - return styled(useStyles(style))( +export const WelcomeDrawerItem: React.FC = function WelcomeDrawerItem({ item, onSelect, disabled }) { + return ( onSelect(item.name)}> - , + ); }; diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomePage.module.css b/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomePage.module.css new file mode 100644 index 0000000000..e9bd9c0a47 --- /dev/null +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomePage.module.css @@ -0,0 +1,10 @@ +.p { + line-height: 2; + white-space: pre-wrap; + margin-block-start: 1em; + margin-block-end: 1em; +} + +.note { + composes: theme-typography--body2 from global; +} diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomePage.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomePage.tsx index 1ec69cf559..c5fd636f58 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomePage.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomePage.tsx @@ -1,41 +1,32 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled, { css } from 'reshadow'; +import { ColoredContainer, Group, GroupItem, s, Translate, useS } from '@cloudbeaver/core-blocks'; -import { ColoredContainer, Group, GroupItem, Translate } from '@cloudbeaver/core-blocks'; - -const styles = css` - p { - line-height: 2; - white-space: pre-wrap; - } - - note { - composes: theme-typography--body2 from global; - } -`; +import styles from './WelcomePage.module.css'; export const WelcomePage: React.FC = function WelcomePage() { - return styled(styles)( + const style = useS(styles); + + return ( -

+

-

+

- +
- +
-
, + ); }; diff --git a/webapp/packages/plugin-administration/src/LocaleService.ts b/webapp/packages/plugin-administration/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-administration/src/LocaleService.ts +++ b/webapp/packages/plugin-administration/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-administration/src/PluginBootstrap.ts b/webapp/packages/plugin-administration/src/PluginBootstrap.ts index 13494d4379..2aebe00b77 100644 --- a/webapp/packages/plugin-administration/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-administration/src/PluginBootstrap.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,19 +8,18 @@ import { lazy } from 'react'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { EAdminPermission } from '@cloudbeaver/core-authentication'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { PermissionsService } from '@cloudbeaver/core-root'; +import { EAdminPermission, PermissionsService } from '@cloudbeaver/core-root'; import { ScreenService } from '@cloudbeaver/core-routing'; -import { DATA_CONTEXT_MENU, MenuBaseItem, MenuService } from '@cloudbeaver/core-view'; +import { MenuBaseItem, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu'; -import { AdministrationTopAppBarService } from './AdministrationScreen/AdministrationTopAppBar/AdministrationTopAppBarService'; +import { AdministrationTopAppBarService } from './AdministrationScreen/AdministrationTopAppBar/AdministrationTopAppBarService.js'; -const AdministrationMenu = lazy(() => import('./AdministrationMenu/AdministrationMenu').then(m => ({ default: m.AdministrationMenu }))); +const AdministrationMenu = lazy(() => import('./AdministrationMenu/AdministrationMenu.js').then(m => ({ default: m.AdministrationMenu }))); const AppStateMenu = lazy(() => import('@cloudbeaver/plugin-top-app-bar').then(m => ({ default: m.AppStateMenu }))); -@injectable() +@injectable(() => [PermissionsService, ScreenService, AdministrationScreenService, AdministrationTopAppBarService, MenuService]) export class PluginBootstrap extends Bootstrap { constructor( private readonly permissionsService: PermissionsService, @@ -32,57 +31,47 @@ export class PluginBootstrap extends Bootstrap { super(); } - register(): void { + override register(): void { this.administrationTopAppBarService.placeholder.add(AdministrationMenu, 0); this.administrationTopAppBarService.placeholder.add(AppStateMenu); + const ADMINISTRATION_MENU_OPEN = new MenuBaseItem( + { + id: 'administrationMenuEnter', + label: 'administration_menu_enter', + tooltip: 'administration_menu_enter', + }, + { onSelect: () => this.administrationScreenService.navigateToRoot() }, + ); + + const ADMINISTRATION_MENU_BACK = new MenuBaseItem( + { + id: 'administrationMenuBack', + label: 'administration_menu_back', + tooltip: 'administration_menu_back', + }, + { onSelect: () => this.screenService.navigateToRoot() }, + ); + this.menuService.addCreator({ - isApplicable: context => context.get(DATA_CONTEXT_MENU) === TOP_NAV_BAR_SETTINGS_MENU, + menus: [TOP_NAV_BAR_SETTINGS_MENU], getItems: (context, items) => { const administrationScreen = this.screenService.isActive(AdministrationScreenService.screenName); if (this.permissionsService.has(EAdminPermission.admin) && !administrationScreen) { - return [ - ...items, - new MenuBaseItem( - { - id: 'administrationMenuEnter', - label: 'administration_menu_enter', - tooltip: 'administration_menu_enter', - }, - { onSelect: () => this.administrationScreenService.navigateToRoot() }, - ), - ]; + return [...items, ADMINISTRATION_MENU_OPEN]; } if (administrationScreen) { - return [ - ...items, - new MenuBaseItem( - { - id: 'administrationMenuBack', - label: 'administration_menu_back', - tooltip: 'administration_menu_back', - }, - { onSelect: () => this.screenService.navigateToRoot() }, - ), - ]; + return [...items, ADMINISTRATION_MENU_BACK]; } return items; }, orderItems: (context, items) => { - const index = items.findIndex(item => item.id === 'administrationMenuBack' || item.id === 'administrationMenuEnter'); - - if (index > -1) { - const item = items.splice(index, 1); - items.unshift(item[0]); - } - + items.unshift(...menuExtractItems(items, [ADMINISTRATION_MENU_OPEN, ADMINISTRATION_MENU_BACK])); return items; }, }); } - - load(): void {} } diff --git a/webapp/packages/plugin-administration/src/index.ts b/webapp/packages/plugin-administration/src/index.ts index db7c1254cd..2a05c489f2 100644 --- a/webapp/packages/plugin-administration/src/index.ts +++ b/webapp/packages/plugin-administration/src/index.ts @@ -1,14 +1,26 @@ -import { manifest } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { manifest } from './manifest.js'; export default manifest; -export * from './AdministrationScreen/AdministrationTopAppBar/AdministrationTopAppBarService'; -export * from './AdministrationScreen/ConfigurationWizard/WizardTopAppBar/WizardTopAppBarService'; -export * from './AdministrationMenu/MENU_APP_ADMINISTRATION_ACTIONS'; -export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationConfigurationForm'; -export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm'; -export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm'; -export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationSecurityForm'; -export * from './ConfigurationWizard/ServerConfiguration/IServerConfigurationPageState'; -export * from './ConfigurationWizard/ServerConfiguration/ServerConfigurationService'; -export * from './ConfigurationWizard/ServerConfigurationAdministrationNavService'; +export * from './AdministrationScreen/AdministrationTopAppBar/AdministrationTopAppBarService.js'; +export * from './AdministrationScreen/ConfigurationWizard/WizardTopAppBar/WizardTopAppBarService.js'; +export * from './AdministrationMenu/MENU_APP_ADMINISTRATION_ACTIONS.js'; +export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationConfigurationForm.js'; +export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.js'; +export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.js'; +export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationSecurityForm.js'; +export * from './ConfigurationWizard/ServerConfiguration/IServerConfigurationPageState.js'; +export * from './ConfigurationWizard/ServerConfiguration/ServerConfigurationService.js'; +export * from './ConfigurationWizard/ServerConfigurationAdministrationNavService.js'; +export * from './ConfigurationWizard/ServerConfiguration/ADMINISTRATION_SERVER_CONFIGURATION_ITEM.js'; +export * from './ConfigurationWizard/ServerConfiguration/IServerConfigurationFormPartState.js'; +export { WELCOME_WIZARD_PAGE_NAME } from './ConfigurationWizard/ConfigurationWizardPagesBootstrapService.js'; diff --git a/webapp/packages/plugin-administration/src/locales/en.ts b/webapp/packages/plugin-administration/src/locales/en.ts index efe0d11eb7..768b05d733 100644 --- a/webapp/packages/plugin-administration/src/locales/en.ts +++ b/webapp/packages/plugin-administration/src/locales/en.ts @@ -3,20 +3,21 @@ 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.', + 'The easy configuration wizard will guide you through several simple steps to set up the server. You will need to set server information and administrator credentials. You can set up additional server parameters once the easy configuration is completed.', ], [ 'administration_configuration_wizard_welcome_note', 'Note: you will be able to change these configuration parameters later on the administration panel.', ], - ['administration_configuration_wizard_configuration', 'Server configuration'], + ['administration_configuration_wizard_configuration', 'Server Configuration'], ['administration_configuration_wizard_configuration_step_description', 'Main server configuration'], ['administration_configuration_wizard_configuration_title', 'You can configure the main server parameters here.'], + ['administration_configuration_wizard_configuration_save_error', 'Failed to save server configuration'], [ 'administration_configuration_wizard_configuration_message', 'You will be able to add additional services after the server configuration.\n\rAdministrator is a super user who can configure server, set databases connections, manage other users and much more. Please, remember the entered password. It is not possible to recover administrator password automatically.', @@ -25,6 +26,26 @@ export default [ ['administration_configuration_tools_save_tooltip', 'Save configuration'], ['administration_configuration_tools_cancel_tooltip', 'Reset changes'], + ['administration_configuration_wizard_configuration_secure_cookies', 'Force HTTPS mode'], + [ + 'administration_configuration_wizard_configuration_secure_cookies_description', + 'Enable force HTTPS to secure server-client communication. Recommended for production. Ensure your HTTPS proxy is properly configured for correct app functionality', + ], + [ + 'administration_configuration_wizard_configuration_secure_cookies_warning', + "The data won't be encrypted if forced HTTPS mode is disabled. This makes it vulnerable", + ], + ['administration_configuration_wizard_configuration_secure_cookies_docs', 'Server proxy configuration documentation'], + ['administration_configuration_wizard_configuration_supported_hosts', 'Allowed Server URLs'], + [ + 'administration_configuration_wizard_configuration_supported_hosts_description', + 'You can specify multiple server URLs separated by a new line. An empty value means that all URLs are allowed', + ], + [ + 'administration_configuration_wizard_configuration_supported_hosts_warning', + 'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.', + ], + ['administration_configuration_wizard_configuration_server_info', 'Server Information'], ['administration_configuration_wizard_configuration_server_name', 'Server Name'], ['administration_configuration_wizard_configuration_server_url', 'Server URL'], @@ -51,11 +72,10 @@ export default [ ['administration_configuration_wizard_configuration_security_admin_credentials', 'Save credentials'], [ 'administration_configuration_wizard_configuration_security_admin_credentials_description', - 'Allow to save credentials for pre-configured database', + 'Allows to save credentials for pre-configured databases', ], ['administration_configuration_wizard_configuration_security_public_credentials', 'Save users credentials'], - ['administration_configuration_wizard_configuration_security_public_credentials_description', 'Allow to save credentials for non-admin users'], - + ['administration_configuration_wizard_configuration_security_public_credentials_description', 'Allows to save credentials for non-admin users'], ['administration_configuration_wizard_configuration_navigator', 'Navigator'], ['administration_configuration_wizard_configuration_navigator_hide_folders', 'Hide Folders'], ['administration_configuration_wizard_configuration_navigator_hide_schemas', 'Hide Schemas'], @@ -65,14 +85,20 @@ export default [ ['administration_configuration_wizard_configuration_navigator_show_system_objects', 'System Objects'], ['administration_configuration_wizard_configuration_navigator_show_utility_objects', 'Utility Objects'], + ['administration_configuration_wizard_step_validation_message', 'Failed to proceed to the next step'], + ['administration_configuration_wizard_finish', 'Confirmation'], ['administration_configuration_wizard_finish_step_description', 'Confirmation'], ['administration_configuration_wizard_finish_title', 'That is almost it.'], [ 'administration_configuration_wizard_finish_message', - 'Press the Finish button to complete the server configuration. You can return to the previous pages if you want to change or add something.\nWhen the configuration is completed all entered settings will be applied for your CloudBeaver server. You will be redirected to the main page to start working.\nYou can always login to the system as administrator to change the server settings.', + 'Press the Finish button to complete the server configuration. You can return to the previous pages if you want to change or add something.\nWhen the configuration is completed all entered settings will be applied for your server. You will be redirected to the main page to start working.\nYou can always login to the system as administrator to change the server settings.', ], ['administration_disabled_drivers_title', 'Disabled drivers'], ['administration_disabled_drivers_search_placeholder', 'Search for the driver...'], + [ + 'administration_disabled_drivers_enable_unsafe_driver_message', + 'Enabling this database driver may allow access to files on the server where this application is running. This could potentially expose sensitive system files or other protected data.\n\nOnly proceed if you fully understand the implications and trust the database configuration. Unauthorized or improper use of this driver may lead to security risks.\n\nDo you want to enable the "{arg:driverName}" driver?', + ], ]; diff --git a/webapp/packages/plugin-administration/src/locales/fr.ts b/webapp/packages/plugin-administration/src/locales/fr.ts new file mode 100644 index 0000000000..047c29880a --- /dev/null +++ b/webapp/packages/plugin-administration/src/locales/fr.ts @@ -0,0 +1,110 @@ +export default [ + ['administration_server_configuration_save_confirmation_title', 'Mise à jour des paramètres du serveur'], + ['administration_server_configuration_save_confirmation_message', 'Vous êtes sur le point de changer des paramètres critiques. Êtes-vous sûr?'], + + ['administration_configuration_wizard_welcome', 'Bienvenue'], + ['administration_configuration_wizard_welcome_step_description', 'Bienvenue sur {alias:product_full_name}'], + ['administration_configuration_wizard_welcome_title', 'Bienvenue sur {alias:product_full_name}, le système de gestion de base de données cloud!'], + [ + 'administration_configuration_wizard_welcome_message', + "L'assistant de configuration facile vous guidera à travers plusieurs étapes simples pour configurer le serveur. Vous devrez définir les informations du serveur et les identifiants de l'administrateur. Vous pourrez configurer des paramètres supplémentaires du serveur une fois la configuration facile terminée.", + ], + [ + 'administration_configuration_wizard_welcome_note', + "Note : vous pourrez changer ces paramètres de configuration plus tard dans le panneau d'administration.", + ], + + ['administration_configuration_wizard_configuration', 'Configuration du serveur'], + ['administration_configuration_wizard_configuration_step_description', 'Configuration principale du serveur'], + ['administration_configuration_wizard_configuration_title', 'Vous pouvez configurer ici les paramètres principaux du serveur.'], + ['administration_configuration_wizard_configuration_save_error', 'Failed to save server configuration'], + [ + 'administration_configuration_wizard_configuration_message', + "Vous pourrez ajouter des services supplémentaires après la configuration du serveur.\nL'administrateur est un super utilisateur qui peut configurer le serveur, définir les connexions aux bases de données, gérer les autres utilisateurs et bien plus encore. Veuillez vous souvenir du mot de passe saisi. Il n'est pas possible de récupérer automatiquement le mot de passe de l'administrateur.", + ], + + ['administration_configuration_tools_save_tooltip', 'Enregistrer la configuration'], + ['administration_configuration_tools_cancel_tooltip', 'Réinitialiser les modifications'], + + ['administration_configuration_wizard_configuration_secure_cookies', 'Force HTTPS mode'], + [ + 'administration_configuration_wizard_configuration_secure_cookies_description', + 'Enable force HTTPS to secure server-client communication. Recommended for production. Ensure your HTTPS proxy is properly configured for correct app functionality', + ], + [ + 'administration_configuration_wizard_configuration_secure_cookies_warning', + "The data won't be encrypted if forced HTTPS mode is disabled. This makes it vulnerable", + ], + ['administration_configuration_wizard_configuration_secure_cookies_docs', 'Server proxy configuration documentation'], + ['administration_configuration_wizard_configuration_supported_hosts', 'Allowed Server URLs'], + [ + 'administration_configuration_wizard_configuration_supported_hosts_description', + 'You can specify multiple server URLs separated by a new line. An empty value means that all URLs are allowed', + ], + [ + 'administration_configuration_wizard_configuration_supported_hosts_warning', + 'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.', + ], + + ['administration_configuration_wizard_configuration_server_info', 'Informations sur le serveur'], + ['administration_configuration_wizard_configuration_server_name', 'Nom du serveur'], + ['administration_configuration_wizard_configuration_server_url', 'URL du serveur'], + ['administration_configuration_wizard_configuration_server_url_description', "URL d'accès global au serveur"], + ['administration_configuration_wizard_configuration_server_info_unsaved_title', 'Paramètres non enregistrés'], + [ + 'administration_configuration_wizard_configuration_server_info_unsaved_message', + 'Les paramètres peuvent être enregistrés sur la page de configuration du serveur', + ], + ['administration_configuration_wizard_configuration_server_info_unsaved_navigate', 'Ouvrir'], + ['administration_configuration_wizard_configuration_server_session_lifetime', 'Durée de la session, min'], + [ + 'administration_configuration_wizard_configuration_server_session_lifetime_description', + "Ici, vous pouvez spécifier le nombre de minutes pendant lesquelles vous souhaitez que la session reste inactive avant qu'elle n'expire", + ], + + ['administration_configuration_wizard_configuration_plugins', 'Configuration'], + ['administration_configuration_wizard_configuration_custom_connections', 'Activer les connexions privées'], + ['administration_configuration_wizard_configuration_custom_connections_description', 'Permet aux utilisateurs de créer des connexions privées'], + ['administration_configuration_wizard_configuration_navigation_tree_view', 'Vue simple du navigateur'], + [ + 'administration_configuration_wizard_configuration_navigation_tree_view_description', + "Par défaut, toutes les nouvelles connexions des utilisateurs ne contiendront que des informations de base dans l'arborescence de navigation", + ], + + ['administration_configuration_wizard_configuration_security', 'Sécurité'], + ['administration_configuration_wizard_configuration_security_admin_credentials', 'Enregistrer les identifiants'], + [ + 'administration_configuration_wizard_configuration_security_admin_credentials_description', + "Permet d'enregistrer les identifiants pour la base de données préconfigurée", + ], + ['administration_configuration_wizard_configuration_security_public_credentials', 'Enregistrer les identifiants des utilisateurs'], + [ + 'administration_configuration_wizard_configuration_security_public_credentials_description', + "Permet d'enregistrer les identifiants pour les utilisateurs non administrateurs", + ], + ['administration_configuration_wizard_configuration_navigator', 'Navigateur'], + ['administration_configuration_wizard_configuration_navigator_hide_folders', 'Masquer les dossiers'], + ['administration_configuration_wizard_configuration_navigator_hide_schemas', 'Masquer les schémas'], + ['administration_configuration_wizard_configuration_navigator_hide_virtual_model', 'Masquer le modèle virtuel'], + ['administration_configuration_wizard_configuration_navigator_merge_entities', 'Fusionner les entités'], + ['administration_configuration_wizard_configuration_navigator_show_only_entities', 'Seulement les entités'], + ['administration_configuration_wizard_configuration_navigator_show_system_objects', 'Objets système'], + ['administration_configuration_wizard_configuration_navigator_show_utility_objects', 'Objets utilitaires'], + + ['administration_configuration_wizard_step_validation_message', "Échec du passage à l'étape suivante"], + + ['administration_configuration_wizard_finish', 'Confirmation'], + ['administration_configuration_wizard_finish_step_description', 'Confirmation'], + ['administration_configuration_wizard_finish_title', "C'est presque terminé."], + [ + 'administration_configuration_wizard_finish_message', + "Appuyez sur le bouton Terminer pour compléter la configuration du serveur. Vous pouvez revenir aux pages précédentes si vous souhaitez modifier ou ajouter quelque chose.\nLorsque la configuration est terminée, tous les paramètres saisis seront appliqués à votre serveur. Vous serez redirigé vers la page principale pour commencer à travailler.\nVous pouvez toujours vous connecter au système en tant qu'administrateur pour modifier les paramètres du serveur.", + ], + + ['administration_disabled_drivers_title', 'Pilotes désactivés'], + ['administration_disabled_drivers_search_placeholder', 'Rechercher le pilote...'], + [ + 'administration_disabled_drivers_enable_unsafe_driver_message', + 'Enabling this database driver may allow access to files on the server where this application is running. This could potentially expose sensitive system files or other protected data.\n\nOnly proceed if you fully understand the implications and trust the database configuration. Unauthorized or improper use of this driver may lead to security risks.\n\nDo you want to enable the "{arg:driverName}" driver?', + ], +]; diff --git a/webapp/packages/plugin-administration/src/locales/it.ts b/webapp/packages/plugin-administration/src/locales/it.ts index f054baf291..0292c6b159 100644 --- a/webapp/packages/plugin-administration/src/locales/it.ts +++ b/webapp/packages/plugin-administration/src/locales/it.ts @@ -7,7 +7,7 @@ export default [ ['administration_configuration_wizard_welcome_title', 'Benvenuto a CloudBeaver, il sistema di gestione database in cloud!'], [ 'administration_configuration_wizard_welcome_message', - 'Il semplice wizard di configurazione ti guiderà per diversi semplici passi per configurare il tuo server CloudBeaver. Dovrai impostare informazioni sul server e le credenziali amministrative. Potrai inoltre aggiungere la tua prima connessione al database.', + 'Il semplice wizard di configurazione ti guiderà per diversi semplici passi per configurare il tuo server. Dovrai impostare informazioni sul server e le credenziali amministrative. Potrai inoltre aggiungere la tua prima connessione al database.', ], [ 'administration_configuration_wizard_welcome_note', @@ -17,6 +17,7 @@ export default [ ['administration_configuration_wizard_configuration', 'Configurazione del Server'], ['administration_configuration_wizard_configuration_step_description', 'Configurazione del server principale'], ['administration_configuration_wizard_configuration_title', 'Puoi configurare i parametri del server principale qui.'], + ['administration_configuration_wizard_configuration_save_error', 'Failed to save server configuration'], [ 'administration_configuration_wizard_configuration_message', "L'amministratore è un super utente che può configurare server, impostare connessioni ai database, gestire altri utenti e molto di più. Si prega di ricordare la password inserita: non sarà possibile recuperarla in maniera automatica.", @@ -25,6 +26,26 @@ export default [ ['administration_configuration_tools_save_tooltip', 'Salva la configurazione'], ['administration_configuration_tools_cancel_tooltip', 'Annulla le modifiche'], + ['administration_configuration_wizard_configuration_secure_cookies', 'Force HTTPS mode'], + [ + 'administration_configuration_wizard_configuration_secure_cookies_description', + 'Enable force HTTPS to secure server-client communication. Recommended for production. Ensure your HTTPS proxy is properly configured for correct app functionality', + ], + [ + 'administration_configuration_wizard_configuration_secure_cookies_warning', + "The data won't be encrypted if forced HTTPS mode is disabled. This makes it vulnerable", + ], + ['administration_configuration_wizard_configuration_secure_cookies_docs', 'Server proxy configuration documentation'], + ['administration_configuration_wizard_configuration_supported_hosts', 'Allowed Server URLs'], + [ + 'administration_configuration_wizard_configuration_supported_hosts_description', + 'You can specify multiple server URLs separated by a new line. An empty value means that all URLs are allowed', + ], + [ + 'administration_configuration_wizard_configuration_supported_hosts_warning', + 'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.', + ], + ['administration_configuration_wizard_configuration_server_info', 'Informazioni sul Server'], ['administration_configuration_wizard_configuration_server_name', 'Nome del Server'], ['administration_configuration_wizard_configuration_server_url', 'Server URL'], @@ -60,7 +81,6 @@ export default [ 'administration_configuration_wizard_configuration_security_public_credentials_description', 'Permetti di salvare le credenziali per gli utenti non amministratori', ], - ['administration_configuration_wizard_configuration_navigator', 'Navigatore'], ['administration_configuration_wizard_configuration_navigator_hide_folders', 'Nascondi le Cartelle'], ['administration_configuration_wizard_configuration_navigator_hide_schemas', 'Nascondi gli Schemi'], @@ -70,14 +90,20 @@ export default [ ['administration_configuration_wizard_configuration_navigator_show_system_objects', 'Oggetti di Sistema'], ['administration_configuration_wizard_configuration_navigator_show_utility_objects', 'Oggetti di UtilitàUtility Objects'], + ['administration_configuration_wizard_step_validation_message', 'Failed to proceed to the next step'], + ['administration_configuration_wizard_finish', 'Conferma'], ['administration_configuration_wizard_finish_step_description', 'Conferma'], ['administration_configuration_wizard_finish_title', 'Ci siamo quasi.'], [ 'administration_configuration_wizard_finish_message', - 'Premi il pulsante Conferma per completare la configurazione del server. Puoi tornare alle pagine precedenti se vuoi modificare o aggiungere qualcosa.\nQuando la configurazione è completa le impostazioni saranno applicate al tuo server CloudBeaver. Sarai portarto alla pagina principale.\nTi ricordiamo che puoi sempre fare login come amministratore e modificare i settaggi del server.', + 'Premi il pulsante Conferma per completare la configurazione del server. Puoi tornare alle pagine precedenti se vuoi modificare o aggiungere qualcosa.\nQuando la configurazione è completa le impostazioni saranno applicate al tuo server. Sarai portarto alla pagina principale.\nTi ricordiamo che puoi sempre fare login come amministratore e modificare i settaggi del server.', ], ['administration_disabled_drivers_title', 'Disabled drivers'], ['administration_disabled_drivers_search_placeholder', 'Search for the driver...'], + [ + 'administration_disabled_drivers_enable_unsafe_driver_message', + 'Enabling this database driver may allow access to files on the server where this application is running. This could potentially expose sensitive system files or other protected data.\n\nOnly proceed if you fully understand the implications and trust the database configuration. Unauthorized or improper use of this driver may lead to security risks.\n\nDo you want to enable the "{arg:driverName}" driver?', + ], ]; diff --git a/webapp/packages/plugin-administration/src/locales/ru.ts b/webapp/packages/plugin-administration/src/locales/ru.ts index aaed836b51..ffc1f1c09c 100644 --- a/webapp/packages/plugin-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-administration/src/locales/ru.ts @@ -3,6 +3,27 @@ export default [ ['administration_server_configuration_save_confirmation_message', 'Будут изменены критичные настройки. Вы уверены?'], ['administration_configuration_wizard_configuration', 'Настройки сервера'], + ['administration_configuration_wizard_configuration_save_error', 'Не удалось сохранить конфигурацию сервера'], + + ['administration_configuration_wizard_configuration_secure_cookies', 'Принудительный HTTPS режим'], + [ + 'administration_configuration_wizard_configuration_secure_cookies_description', + 'Включите принудительный HTTPS для защиты связи между сервером и клиентами. Рекомендуется для рабочих окружений. Убедитесь, что ваш HTTPS-прокси правильно настроен для корректной работы приложения', + ], + [ + 'administration_configuration_wizard_configuration_secure_cookies_warning', + 'Данные не будут зашифрованы, если принудительный HTTPS режим выключен. Это делает их уязвимыми', + ], + ['administration_configuration_wizard_configuration_secure_cookies_docs', 'Документация по настройке сервер-прокси'], + ['administration_configuration_wizard_configuration_supported_hosts', 'Разрешённые URL сервера'], + [ + 'administration_configuration_wizard_configuration_supported_hosts_description', + 'Вы можете указать несколько URL серверов, разделенных новой строкой. Пустое значение означает, что все URL разрешены', + ], + [ + 'administration_configuration_wizard_configuration_supported_hosts_warning', + 'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.', + ], ['administration_configuration_wizard_configuration_server_info', 'Информация о сервере'], ['administration_configuration_wizard_configuration_server_name', 'Название сервера'], @@ -26,18 +47,24 @@ export default [ 'Все новые подключения, созданные пользователем, будут иметь только базовую информацию в дереве навигации', ], + ['administration_configuration_wizard_step_validation_message', 'Не удалось перейти к следующему шагу'], + ['administration_configuration_wizard_configuration_security', 'Безопасность'], ['administration_configuration_wizard_configuration_security_admin_credentials', 'Позволить сохранять приватные данные'], ['administration_configuration_wizard_configuration_security_public_credentials', 'Позволить сохранять приватные данные для пользователей'], [ 'administration_configuration_wizard_configuration_security_admin_credentials_description', - 'Позволяет сохранять приватные данные, такие как пароли и SSH ключи', + 'Позволяет сохранять приватные данные для настроенных подключений', ], [ 'administration_configuration_wizard_configuration_security_public_credentials_description', - 'Пользователи будут иметь возможность сохранять приватные данные, такие как пароли и SSH ключи', + 'Позволяет сохранять приватные данные (такие как пароли и SSH ключи) для пользователей, не являющихся администраторами', ], ['administration_disabled_drivers_title', 'Отключенные драйверы'], ['administration_disabled_drivers_search_placeholder', 'Поиск по драйверу...'], + [ + 'administration_disabled_drivers_enable_unsafe_driver_message', + 'Включение этого драйвера базы данных может позволить доступ к файлам на сервере, где работает это приложение. Это может привести к потенциальному раскрытию конфиденциальных системных файлов или другой защищённой информации.\n\nПродолжайте только в том случае, если вы полностью понимаете последствия и уверены в безопасности использования этого драйвера. Неавторизованное или ненадлежащее использование может привести к проблемам с безопасностью.\n\nВы действительно хотите включить "{arg:driverName}" драйвер?', + ], ]; diff --git a/webapp/packages/plugin-administration/src/locales/vi.ts b/webapp/packages/plugin-administration/src/locales/vi.ts new file mode 100644 index 0000000000..546c96da5f --- /dev/null +++ b/webapp/packages/plugin-administration/src/locales/vi.ts @@ -0,0 +1,94 @@ +export default [ + ['administration_server_configuration_save_confirmation_title', 'Cập nhật cài đặt server'], + ['administration_server_configuration_save_confirmation_message', 'Bạn sắp thay đổi các cài đặt quan trọng. Bạn có chắc chắn không?'], + ['administration_configuration_wizard_welcome', 'Chào mừng'], + ['administration_configuration_wizard_welcome_step_description', 'Chào mừng đến với {alias:product_full_name}'], + ['administration_configuration_wizard_welcome_title', 'Chào mừng đến với {alias:product_full_name}, hệ thống quản lý cơ sở dữ liệu đám mây!'], + [ + 'administration_configuration_wizard_welcome_message', + 'Trình hướng dẫn cấu hình dễ dàng sẽ dẫn bạn qua một số bước đơn giản để thiết lập server. Bạn cần thiết lập thông tin server và thông tin xác thực quản trị viên. Bạn có thể thiết lập thêm các tham số server sau khi hoàn tất cấu hình dễ dàng.', + ], + ['administration_configuration_wizard_welcome_note', 'Lưu ý: bạn sẽ có thể thay đổi các tham số cấu hình này sau trong bảng quản trị.'], + ['administration_configuration_wizard_configuration', 'Cấu hình Server'], + ['administration_configuration_wizard_configuration_step_description', 'Cấu hình server chính'], + ['administration_configuration_wizard_configuration_title', 'Bạn có thể cấu hình các tham số server chính tại đây.'], + ['administration_configuration_wizard_configuration_save_error', 'Không thể lưu cấu hình server'], + [ + 'administration_configuration_wizard_configuration_message', + 'Bạn sẽ có thể thêm các dịch vụ bổ sung sau khi cấu hình server.\n\rQuản trị viên là siêu người dùng có thể cấu hình server, thiết lập kết nối cơ sở dữ liệu, quản lý người dùng khác và nhiều hơn nữa. Vui lòng ghi nhớ mật khẩu đã nhập. Không thể khôi phục mật khẩu quản trị viên tự động.', + ], + ['administration_configuration_wizard_configuration_secure_cookies', 'Force HTTPS mode'], + [ + 'administration_configuration_wizard_configuration_secure_cookies_description', + 'Enable force HTTPS to secure server-client communication. Recommended for production. Ensure your HTTPS proxy is properly configured for correct app functionality', + ], + [ + 'administration_configuration_wizard_configuration_secure_cookies_warning', + "The data won't be encrypted if forced HTTPS mode is disabled. This makes it vulnerable", + ], + ['administration_configuration_wizard_configuration_secure_cookies_docs', 'Server proxy configuration documentation'], + ['administration_configuration_wizard_configuration_supported_hosts', 'Allowed Server URLs'], + [ + 'administration_configuration_wizard_configuration_supported_hosts_description', + 'You can specify multiple server URLs separated by a new line. An empty value means that all URLs are allowed', + ], + [ + 'administration_configuration_wizard_configuration_supported_hosts_warning', + 'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.', + ], + ['administration_configuration_tools_save_tooltip', 'Lưu cấu hình'], + ['administration_configuration_tools_cancel_tooltip', 'Đặt lại thay đổi'], + ['administration_configuration_wizard_configuration_server_info', 'Thông tin Server'], + ['administration_configuration_wizard_configuration_server_name', 'Tên Server'], + ['administration_configuration_wizard_configuration_server_url', 'URL Server'], + ['administration_configuration_wizard_configuration_server_url_description', 'URL truy cập toàn cục của server'], + ['administration_configuration_wizard_configuration_server_info_unsaved_title', 'Cài đặt chưa được lưu'], + ['administration_configuration_wizard_configuration_server_info_unsaved_message', 'Cài đặt có thể được lưu trên trang Cấu hình Server'], + ['administration_configuration_wizard_configuration_server_info_unsaved_navigate', 'Mở'], + ['administration_configuration_wizard_configuration_server_session_lifetime', 'Thời gian phiên, phút'], + [ + 'administration_configuration_wizard_configuration_server_session_lifetime_description', + 'Tại đây, bạn có thể chỉ định số phút mà phiên được phép ở trạng thái không hoạt động trước khi hết hạn', + ], + ['administration_configuration_wizard_configuration_plugins', 'Cấu hình'], + ['administration_configuration_wizard_configuration_custom_connections', 'Bật kết nối riêng tư'], + ['administration_configuration_wizard_configuration_custom_connections_description', 'Cho phép người dùng tạo kết nối riêng tư'], + ['administration_configuration_wizard_configuration_navigation_tree_view', 'Chế độ xem đơn giản của Navigator'], + [ + 'administration_configuration_wizard_configuration_navigation_tree_view_description', + 'Theo mặc định, tất cả kết nối mới của người dùng sẽ chỉ chứa thông tin cơ bản trong cây điều hướng', + ], + ['administration_configuration_wizard_configuration_security', 'Bảo mật'], + ['administration_configuration_wizard_configuration_security_admin_credentials', 'Lưu thông tin xác thực'], + [ + 'administration_configuration_wizard_configuration_security_admin_credentials_description', + 'Cho phép lưu thông tin xác thực cho các cơ sở dữ liệu được cấu hình sẵn', + ], + ['administration_configuration_wizard_configuration_security_public_credentials', 'Lưu thông tin xác thực của người dùng'], + [ + 'administration_configuration_wizard_configuration_security_public_credentials_description', + 'Cho phép lưu thông tin xác thực cho người dùng không phải quản trị viên', + ], + ['administration_configuration_wizard_configuration_navigator', 'Navigator'], + ['administration_configuration_wizard_configuration_navigator_hide_folders', 'Ẩn Thư mục'], + ['administration_configuration_wizard_configuration_navigator_hide_schemas', 'Ẩn Lược đồ'], + ['administration_configuration_wizard_configuration_navigator_hide_virtual_model', 'Ẩn Mô hình Ảo'], + ['administration_configuration_wizard_configuration_navigator_merge_entities', 'Hợp nhất Thực thể'], + ['administration_configuration_wizard_configuration_navigator_show_only_entities', 'Chỉ Thực thể'], + ['administration_configuration_wizard_configuration_navigator_show_system_objects', 'Đối tượng Hệ thống'], + ['administration_configuration_wizard_configuration_navigator_show_utility_objects', 'Đối tượng Tiện ích'], + ['administration_configuration_wizard_step_validation_message', 'Không thể chuyển sang bước tiếp theo'], + ['administration_configuration_wizard_finish', 'Xác nhận'], + ['administration_configuration_wizard_finish_step_description', 'Xác nhận'], + ['administration_configuration_wizard_finish_title', 'Gần hoàn tất.'], + [ + 'administration_configuration_wizard_finish_message', + 'Nhấn nút Hoàn tất để hoàn tất cấu hình server. Bạn có thể quay lại các trang trước nếu muốn thay đổi hoặc thêm gì đó.\nKhi cấu hình hoàn tất, tất cả cài đặt đã nhập sẽ được áp dụng cho server của bạn. Bạn sẽ được chuyển hướng đến trang chính để bắt đầu làm việc.\nBạn luôn có thể đăng nhập vào hệ thống với tư cách quản trị viên để thay đổi cài đặt server.', + ], + ['administration_disabled_drivers_title', 'Trình điều khiển (driver) bị tắt'], + ['administration_disabled_drivers_search_placeholder', 'Tìm kiếm trình điều khiển (driver)...'], + [ + 'administration_disabled_drivers_enable_unsafe_driver_message', + 'Việc bật trình điều khiển (driver) cơ sở dữ liệu này có thể cho phép truy cập vào các tệp trên server nơi ứng dụng này đang chạy. Điều này có thể làm lộ các tệp hệ thống nhạy cảm hoặc dữ liệu được bảo vệ khác.\n\nChỉ tiếp tục nếu bạn hiểu đầy đủ các rủi ro và tin tưởng vào cấu hình cơ sở dữ liệu. Việc sử dụng trình điều khiển này không được phép hoặc không đúng cách có thể dẫn đến rủi ro bảo mật.\n\nBạn có muốn bật trình điều khiển "{arg:driverName}" không?', + ], +]; diff --git a/webapp/packages/plugin-administration/src/locales/zh.ts b/webapp/packages/plugin-administration/src/locales/zh.ts index 1dc16e6cb9..f7c3100bbb 100644 --- a/webapp/packages/plugin-administration/src/locales/zh.ts +++ b/webapp/packages/plugin-administration/src/locales/zh.ts @@ -14,6 +14,7 @@ export default [ ['administration_configuration_wizard_configuration', '服务器配置'], ['administration_configuration_wizard_configuration_step_description', '主要的服务器配置'], ['administration_configuration_wizard_configuration_title', '您可以在这里配置主要的服务器参数'], + ['administration_configuration_wizard_configuration_save_error', 'Failed to save server configuration'], [ 'administration_configuration_wizard_configuration_message', '管理员是一个超级用户,可以配置服务器、设置数据库连接、管理其他用户等等。请记住输入的密码。无法自动恢复管理员密码。', @@ -22,6 +23,26 @@ export default [ ['administration_configuration_tools_save_tooltip', '保存配置'], ['administration_configuration_tools_cancel_tooltip', '重置更改'], + ['administration_configuration_wizard_configuration_secure_cookies', 'Force HTTPS mode'], + [ + 'administration_configuration_wizard_configuration_secure_cookies_description', + 'Enable force HTTPS to secure server-client communication. Recommended for production. Ensure your HTTPS proxy is properly configured for correct app functionality', + ], + [ + 'administration_configuration_wizard_configuration_secure_cookies_warning', + "The data won't be encrypted if forced HTTPS mode is disabled. This makes it vulnerable", + ], + ['administration_configuration_wizard_configuration_secure_cookies_docs', 'Server proxy configuration documentation'], + ['administration_configuration_wizard_configuration_supported_hosts', 'Allowed Server URLs'], + [ + 'administration_configuration_wizard_configuration_supported_hosts_description', + 'You can specify multiple server URLs separated by a new line. An empty value means that all URLs are allowed', + ], + [ + 'administration_configuration_wizard_configuration_supported_hosts_warning', + 'You cannot remove your current domain ({arg:host}). Open the server configuration from another allowed domain or IP-address to remove this domain.', + ], + ['administration_configuration_wizard_configuration_server_info', '服务器信息'], ['administration_configuration_wizard_configuration_server_name', '服务器名称'], ['administration_configuration_wizard_configuration_server_url', '服务器URL'], @@ -52,6 +73,8 @@ export default [ ['administration_configuration_wizard_configuration_navigator_show_system_objects', '系统对象'], ['administration_configuration_wizard_configuration_navigator_show_utility_objects', '实用程序对象'], + ['administration_configuration_wizard_step_validation_message', '无法继续执行下一步'], + ['administration_configuration_wizard_finish', '确认'], ['administration_configuration_wizard_finish_step_description', '确认'], ['administration_configuration_wizard_finish_title', '差不多就这样。'], @@ -60,6 +83,10 @@ export default [ '按完成按钮完成服务器配置。如果您想更改或添加某些内容,可以返回到前面的页面。\n配置完成后,所有输入的设置都将应用于您的CloudBeaver服务器。您将被重定向到主页面开始工作。\n您可以随时以管理员身份登录系统以更改服务器设置。', ], - ['administration_disabled_drivers_title', 'Disabled drivers'], - ['administration_disabled_drivers_search_placeholder', 'Search for the driver...'], + ['administration_disabled_drivers_title', '已禁用的驱动'], + ['administration_disabled_drivers_search_placeholder', '搜索驱动...'], + [ + 'administration_disabled_drivers_enable_unsafe_driver_message', + 'Enabling this database driver may allow access to files on the server where this application is running. This could potentially expose sensitive system files or other protected data.\n\nOnly proceed if you fully understand the implications and trust the database configuration. Unauthorized or improper use of this driver may lead to security risks.\n\nDo you want to enable the "{arg:driverName}" driver?', + ], ]; diff --git a/webapp/packages/plugin-administration/src/manifest.ts b/webapp/packages/plugin-administration/src/manifest.ts index 466856d231..105daaf2ca 100644 --- a/webapp/packages/plugin-administration/src/manifest.ts +++ b/webapp/packages/plugin-administration/src/manifest.ts @@ -1,36 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { AdministrationViewService } from './Administration/AdministrationViewService'; -import { AdministrationScreenServiceBootstrap } from './AdministrationScreen/AdministrationScreenServiceBootstrap'; -import { AdministrationTopAppBarService } from './AdministrationScreen/AdministrationTopAppBar/AdministrationTopAppBarService'; -import { WizardTopAppBarService } from './AdministrationScreen/ConfigurationWizard/WizardTopAppBar/WizardTopAppBarService'; -import { ConfigurationWizardPagesBootstrapService } from './ConfigurationWizard/ConfigurationWizardPagesBootstrapService'; -import { ServerConfigurationService } from './ConfigurationWizard/ServerConfiguration/ServerConfigurationService'; -import { ServerConfigurationAdministrationNavService } from './ConfigurationWizard/ServerConfigurationAdministrationNavService'; -import { LocaleService } from './LocaleService'; -import { PluginBootstrap } from './PluginBootstrap'; - export const manifest: PluginManifest = { info: { name: 'Authentication', }, - - providers: [ - LocaleService, - PluginBootstrap, - ServerConfigurationService, - ServerConfigurationAdministrationNavService, - ConfigurationWizardPagesBootstrapService, - AdministrationScreenServiceBootstrap, - AdministrationTopAppBarService, - WizardTopAppBarService, - AdministrationViewService, - ], }; diff --git a/webapp/packages/plugin-administration/src/module.ts b/webapp/packages/plugin-administration/src/module.ts new file mode 100644 index 0000000000..acd690bce5 --- /dev/null +++ b/webapp/packages/plugin-administration/src/module.ts @@ -0,0 +1,42 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { PluginBootstrap } from './PluginBootstrap.js'; +import { LocaleService } from './LocaleService.js'; +import { ServerConfigurationAdministrationNavService } from './ConfigurationWizard/ServerConfigurationAdministrationNavService.js'; +import { ServerConfigurationService } from './ConfigurationWizard/ServerConfiguration/ServerConfigurationService.js'; +import { ServerConfigurationFormService } from './ConfigurationWizard/ServerConfiguration/ServerConfigurationFormService.js'; +import { ServerConfigurationFormStateManager } from './ConfigurationWizard/ServerConfiguration/ServerConfigurationFormStateManager.js'; +import { AdministrationTopAppBarService } from './AdministrationScreen/AdministrationTopAppBar/AdministrationTopAppBarService.js'; +import { ConfigurationWizardPagesBootstrapService } from './ConfigurationWizard/ConfigurationWizardPagesBootstrapService.js'; +import { WizardTopAppBarService } from './AdministrationScreen/ConfigurationWizard/WizardTopAppBar/WizardTopAppBarService.js'; +import { AdministrationScreenServiceBootstrap } from './AdministrationScreen/AdministrationScreenServiceBootstrap.js'; +import { AdministrationViewService } from './Administration/AdministrationViewService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-administration', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, proxy(AdministrationScreenServiceBootstrap)) + .addSingleton(Bootstrap, proxy(ConfigurationWizardPagesBootstrapService)) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(Bootstrap, proxy(PluginBootstrap)) + .addSingleton(AdministrationViewService) + .addSingleton(PluginBootstrap) + .addSingleton(ServerConfigurationAdministrationNavService) + .addSingleton(ServerConfigurationService) + .addSingleton(ServerConfigurationFormService) + .addSingleton(ServerConfigurationFormStateManager) + .addSingleton(AdministrationTopAppBarService) + .addSingleton(ConfigurationWizardPagesBootstrapService) + .addSingleton(WizardTopAppBarService) + .addSingleton(AdministrationScreenServiceBootstrap); + }, +}); diff --git a/webapp/packages/plugin-administration/tsconfig.json b/webapp/packages/plugin-administration/tsconfig.json index 58fb094fb5..75397c1292 100644 --- a/webapp/packages/plugin-administration/tsconfig.json +++ b/webapp/packages/plugin-administration/tsconfig.json @@ -1,61 +1,77 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-settings-menu/tsconfig.json" + "path": "../../common-typescript/@dbeaver/js-helpers" }, { - "path": "../plugin-top-app-bar/tsconfig.json" + "path": "../core-administration" }, { - "path": "../core-administration/tsconfig.json" + "path": "../core-authentication" }, { - "path": "../core-authentication/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-connections" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-executor/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-executor" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-links" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-routing/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-routing" }, { - "path": "../core-view/tsconfig.json" + "path": "../core-sdk" + }, + { + "path": "../core-ui" + }, + { + "path": "../core-utils" + }, + { + "path": "../core-view" + }, + { + "path": "../plugin-settings-menu" + }, + { + "path": "../plugin-top-app-bar" } ], "include": [ @@ -67,7 +83,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-connection-template/.gitignore b/webapp/packages/plugin-app-logo-administration/.gitignore similarity index 100% rename from webapp/packages/plugin-connection-template/.gitignore rename to webapp/packages/plugin-app-logo-administration/.gitignore diff --git a/webapp/packages/plugin-app-logo-administration/package.json b/webapp/packages/plugin-app-logo-administration/package.json new file mode 100644 index 0000000000..e8ff4964b6 --- /dev/null +++ b/webapp/packages/plugin-app-logo-administration/package.json @@ -0,0 +1,34 @@ +{ + "name": "@cloudbeaver/plugin-app-logo-administration", + "type": "module", + "sideEffects": [ + "./lib/module.js", + "./lib/index.js", + "src/**/*.css", + "src/**/*.scss", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "exports": { + ".": "./lib/index.js" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf --glob lib", + "lint": "eslint ./src/ --ext .ts,.tsx", + "validate-dependencies": "core-cli-validate-dependencies" + }, + "dependencies": { + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/plugin-administration": "workspace:*", + "@cloudbeaver/plugin-app-logo": "workspace:*", + "tslib": "^2" + }, + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "typescript": "^5" + } +} diff --git a/webapp/packages/plugin-app-logo-administration/src/PluginBootstrap.ts b/webapp/packages/plugin-app-logo-administration/src/PluginBootstrap.ts new file mode 100644 index 0000000000..5af064dffd --- /dev/null +++ b/webapp/packages/plugin-app-logo-administration/src/PluginBootstrap.ts @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { AdministrationTopAppBarService, WizardTopAppBarService } from '@cloudbeaver/plugin-administration'; +import { LogoLazy } from '@cloudbeaver/plugin-app-logo'; + +@injectable(() => [AdministrationTopAppBarService, WizardTopAppBarService]) +export class AppLogoAdministrationPluginBootstrap extends Bootstrap { + constructor( + private readonly administrationTopAppBarService: AdministrationTopAppBarService, + private readonly wizardTopAppBarService: WizardTopAppBarService, + ) { + super(); + } + + override register() { + this.administrationTopAppBarService.placeholder.add(LogoLazy, 0); + this.wizardTopAppBarService.placeholder.add(LogoLazy, 0); + } +} diff --git a/webapp/packages/plugin-app-logo-administration/src/index.ts b/webapp/packages/plugin-app-logo-administration/src/index.ts new file mode 100644 index 0000000000..e1983eabfc --- /dev/null +++ b/webapp/packages/plugin-app-logo-administration/src/index.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { appLogoAdministrationPlugin } from './manifest.js'; + +export { appLogoAdministrationPlugin }; +export default appLogoAdministrationPlugin; diff --git a/webapp/packages/plugin-app-logo-administration/src/manifest.ts b/webapp/packages/plugin-app-logo-administration/src/manifest.ts new file mode 100644 index 0000000000..99790b16cb --- /dev/null +++ b/webapp/packages/plugin-app-logo-administration/src/manifest.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; + +export const appLogoAdministrationPlugin: PluginManifest = { + info: { + name: 'App Logo Administration plugin', + }, +}; diff --git a/webapp/packages/plugin-app-logo-administration/src/module.ts b/webapp/packages/plugin-app-logo-administration/src/module.ts new file mode 100644 index 0000000000..ac073574eb --- /dev/null +++ b/webapp/packages/plugin-app-logo-administration/src/module.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, ModuleRegistry } from '@cloudbeaver/core-di'; +import { AppLogoAdministrationPluginBootstrap } from './PluginBootstrap.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-app-logo-administration', + + configure: serviceCollection => { + serviceCollection.addSingleton(Bootstrap, AppLogoAdministrationPluginBootstrap); + }, +}); diff --git a/webapp/packages/plugin-app-logo-administration/tsconfig.json b/webapp/packages/plugin-app-logo-administration/tsconfig.json new file mode 100644 index 0000000000..85268bcfbe --- /dev/null +++ b/webapp/packages/plugin-app-logo-administration/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "@cloudbeaver/tsconfig/tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true + }, + "references": [ + { + "path": "../core-cli" + }, + { + "path": "../core-di" + }, + { + "path": "../plugin-administration" + }, + { + "path": "../plugin-app-logo" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*" + ] +} diff --git a/webapp/packages/plugin-app-logo/.gitignore b/webapp/packages/plugin-app-logo/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-app-logo/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-app-logo/package.json b/webapp/packages/plugin-app-logo/package.json new file mode 100644 index 0000000000..4127cbe103 --- /dev/null +++ b/webapp/packages/plugin-app-logo/package.json @@ -0,0 +1,43 @@ +{ + "name": "@cloudbeaver/plugin-app-logo", + "type": "module", + "sideEffects": [ + "./lib/module.js", + "./lib/index.js", + "src/**/*.css", + "src/**/*.scss", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "exports": { + ".": "./lib/index.js" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf --glob lib", + "lint": "eslint ./src/ --ext .ts,.tsx", + "validate-dependencies": "core-cli-validate-dependencies" + }, + "dependencies": { + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-routing": "workspace:*", + "@cloudbeaver/core-version": "workspace:*", + "@cloudbeaver/plugin-holidays": "workspace:*", + "@cloudbeaver/plugin-top-app-bar": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" + }, + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5" + } +} diff --git a/webapp/packages/plugin-app-logo/src/Logo.tsx b/webapp/packages/plugin-app-logo/src/Logo.tsx new file mode 100644 index 0000000000..1961e87a65 --- /dev/null +++ b/webapp/packages/plugin-app-logo/src/Logo.tsx @@ -0,0 +1,33 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { AppLogo, useResource } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { PermissionsService, ProductInfoResource } from '@cloudbeaver/core-root'; +import { ScreenService } from '@cloudbeaver/core-routing'; +import { useAppVersion } from '@cloudbeaver/core-version'; +import { HolidaysService } from '@cloudbeaver/plugin-holidays'; + +export const Logo = observer(function Logo() { + const productInfoResource = useResource(Logo, ProductInfoResource, undefined); + const screenService = useService(ScreenService); + const permissionsService = useService(PermissionsService); + const { backendVersion, frontendVersion } = useAppVersion(true); + const { holiday } = useService(HolidaysService); + + const isSameVersion = backendVersion === frontendVersion; + + const productName = productInfoResource.data?.name || 'CloudBeaver'; + const backendVersionTitle = `${productName} ver. ${backendVersion}`; + const commonVersionTitle = `${productName} ver. ${frontendVersion}(${backendVersion})`; + + const title = isSameVersion ? backendVersionTitle : commonVersionTitle; + + return ; +}); diff --git a/webapp/packages/plugin-app-logo/src/LogoLazy.tsx b/webapp/packages/plugin-app-logo/src/LogoLazy.tsx new file mode 100644 index 0000000000..fbcc4390ff --- /dev/null +++ b/webapp/packages/plugin-app-logo/src/LogoLazy.tsx @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const LogoLazy = importLazyComponent(() => import('./Logo.js').then(m => m.Logo)); diff --git a/webapp/packages/plugin-app-logo/src/PluginBootstrap.ts b/webapp/packages/plugin-app-logo/src/PluginBootstrap.ts new file mode 100644 index 0000000000..921ab25709 --- /dev/null +++ b/webapp/packages/plugin-app-logo/src/PluginBootstrap.ts @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { TopNavService } from '@cloudbeaver/plugin-top-app-bar'; +import { LogoLazy } from './LogoLazy.js'; + +@injectable(() => [TopNavService]) +export class AppLogoPluginBootstrap extends Bootstrap { + constructor(private readonly topNavService: TopNavService) { + super(); + } + + override register() { + this.topNavService.placeholder.add(LogoLazy, 0); + } +} diff --git a/webapp/packages/plugin-app-logo/src/index.ts b/webapp/packages/plugin-app-logo/src/index.ts new file mode 100644 index 0000000000..925e632545 --- /dev/null +++ b/webapp/packages/plugin-app-logo/src/index.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { appLogoPlugin } from './manifest.js'; + +export * from './LogoLazy.js'; + +export { appLogoPlugin }; +export default appLogoPlugin; diff --git a/webapp/packages/plugin-app-logo/src/manifest.ts b/webapp/packages/plugin-app-logo/src/manifest.ts new file mode 100644 index 0000000000..9e2cf7bc4f --- /dev/null +++ b/webapp/packages/plugin-app-logo/src/manifest.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; + +export const appLogoPlugin: PluginManifest = { + info: { + name: 'App Logo plugin', + }, +}; diff --git a/webapp/packages/plugin-app-logo/src/module.ts b/webapp/packages/plugin-app-logo/src/module.ts new file mode 100644 index 0000000000..43be9c5c98 --- /dev/null +++ b/webapp/packages/plugin-app-logo/src/module.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ModuleRegistry, Bootstrap } from '@cloudbeaver/core-di'; +import { AppLogoPluginBootstrap } from './PluginBootstrap.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-app-logo', + + configure: serviceCollection => { + serviceCollection.addSingleton(Bootstrap, AppLogoPluginBootstrap); + }, +}); diff --git a/webapp/packages/plugin-app-logo/tsconfig.json b/webapp/packages/plugin-app-logo/tsconfig.json new file mode 100644 index 0000000000..913ce6ac97 --- /dev/null +++ b/webapp/packages/plugin-app-logo/tsconfig.json @@ -0,0 +1,46 @@ +{ + "extends": "@cloudbeaver/tsconfig/tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true + }, + "references": [ + { + "path": "../core-blocks" + }, + { + "path": "../core-cli" + }, + { + "path": "../core-di" + }, + { + "path": "../core-root" + }, + { + "path": "../core-routing" + }, + { + "path": "../core-version" + }, + { + "path": "../plugin-holidays" + }, + { + "path": "../plugin-top-app-bar" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*" + ] +} diff --git a/webapp/packages/plugin-authentication-administration/package.json b/webapp/packages/plugin-authentication-administration/package.json index 813ac13f25..f936b84517 100644 --- a/webapp/packages/plugin-authentication-administration/package.json +++ b/webapp/packages/plugin-authentication-administration/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-authentication-administration", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,40 +11,49 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-administration": "~0.1.0", - "@cloudbeaver/plugin-authentication": "~0.1.0", - "@cloudbeaver/core-administration": "~0.1.0", - "@cloudbeaver/core-authentication": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-data-context": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-projects": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-administration": "workspace:*", + "@cloudbeaver/core-authentication": "workspace:*", + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-projects": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-administration": "workspace:*", + "@cloudbeaver/plugin-authentication": "workspace:*", + "@dbeaver/js-helpers": "workspace:^", + "@dbeaver/ui-kit": "workspace:^", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "reakit": "^1", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationForm.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationForm.tsx deleted file mode 100644 index 05a56d9065..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationForm.tsx +++ /dev/null @@ -1,150 +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 { observer } from 'mobx-react-lite'; -import { useEffect } from 'react'; -import styled, { css } from 'reshadow'; - -import { IconOrImage, Loader, Placeholder, useExecutor, useObjectRef, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import type { AdminAuthProviderConfiguration } from '@cloudbeaver/core-sdk'; -import { BASE_TAB_STYLES, TabList, TabPanelList, TabsState, UNDERLINE_TAB_BIG_STYLES, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui'; - -import { AuthConfigurationFormService } from './AuthConfigurationFormService'; -import { authConfigurationContext } from './Contexts/authConfigurationContext'; -import type { IAuthConfigurationFormState } from './IAuthConfigurationFormProps'; - -const tabsStyles = css` - TabList { - position: relative; - flex-shrink: 0; - align-items: center; - } -`; - -const topBarStyles = css` - configuration-top-bar { - composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global; - position: relative; - display: flex; - padding-top: 16px; - - &:before { - content: ''; - position: absolute; - bottom: 0; - width: 100%; - border-bottom: solid 2px; - border-color: inherit; - } - } - configuration-top-bar-tabs { - flex: 1; - } - - configuration-top-bar-actions { - display: flex; - align-items: center; - padding: 0 24px; - gap: 16px; - } - - configuration-status-message { - composes: theme-typography--caption from global; - height: 24px; - padding: 0 16px; - display: flex; - align-items: center; - gap: 8px; - - & IconOrImage { - height: 24px; - width: 24px; - } - } -`; - -const formStyles = css` - box { - composes: theme-background-secondary theme-text-on-secondary from global; - display: flex; - flex-direction: column; - flex: 1; - height: 100%; - overflow: auto; - } - content-box { - composes: theme-background-secondary theme-border-color-background from global; - position: relative; - display: flex; - flex: 1; - flex-direction: column; - overflow: auto; - } -`; - -interface Props { - state: IAuthConfigurationFormState; - onCancel?: () => void; - onSave?: (configuration: AdminAuthProviderConfiguration) => void; - className?: string; -} - -export const AuthConfigurationForm = observer(function AuthConfigurationForm({ state, onCancel, onSave = () => {}, className }) { - const translate = useTranslate(); - const props = useObjectRef({ onSave }); - const style = useStyles(BASE_TAB_STYLES, tabsStyles, UNDERLINE_TAB_STYLES, UNDERLINE_TAB_BIG_STYLES); - const styles = useStyles(style, topBarStyles, formStyles); - const service = useService(AuthConfigurationFormService); - - useExecutor({ - executor: state.submittingTask, - postHandlers: [ - function save(data, contexts) { - const validation = contexts.getContext(service.configurationValidationContext); - const state = contexts.getContext(service.configurationStatusContext); - const config = contexts.getContext(authConfigurationContext); - - if (validation.valid && state.saved) { - props.onSave(config); - } - }, - ], - }); - - useEffect(() => { - state.loadConfigurationInfo(); - }, []); - - return styled(styles)( - - - - - - {state.statusMessage && ( - <> - - {translate(state.statusMessage)} - - )} - - - - - - - - - - - - - - , - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationFormBaseActions.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationFormBaseActions.tsx deleted file mode 100644 index 8bf605644b..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationFormBaseActions.tsx +++ /dev/null @@ -1,31 +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 { observer } from 'mobx-react-lite'; - -import { Button, PlaceholderComponent, useTranslate } from '@cloudbeaver/core-blocks'; - -import type { IAuthConfigurationFormProps } from './IAuthConfigurationFormProps'; - -export const AuthConfigurationFormBaseActions: PlaceholderComponent = observer( - function AuthConfigurationFormBaseActions({ state, onCancel }) { - const translate = useTranslate(); - - return ( - <> - {onCancel && ( - - )} - - - ); - }, -); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationFormService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationFormService.ts deleted file mode 100644 index 0b66537432..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationFormService.ts +++ /dev/null @@ -1,143 +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 React from 'react'; - -import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; -import { injectable } from '@cloudbeaver/core-di'; -import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; -import { ExecutorHandlersCollection, ExecutorInterrupter, IExecutorHandler, IExecutorHandlersCollection } from '@cloudbeaver/core-executor'; -import { TabsContainer } from '@cloudbeaver/core-ui'; - -import type { - IAuthConfigurationFormFillConfigData, - IAuthConfigurationFormProps, - IAuthConfigurationFormState, - IAuthConfigurationFormSubmitData, -} from './IAuthConfigurationFormProps'; - -const AuthConfigurationFormBaseActions = React.lazy(async () => { - const { AuthConfigurationFormBaseActions } = await import('./AuthConfigurationFormBaseActions'); - return { default: AuthConfigurationFormBaseActions }; -}); - -export interface IConfigurationFormValidation { - valid: boolean; - messages: string[]; - info: (message: string) => void; - error: (message: string) => void; -} - -export interface IConfigurationFormStatus { - saved: boolean; - messages: string[]; - exception: Error | null; - info: (message: string) => void; - error: (message: string, exception?: Error) => void; -} - -@injectable() -export class AuthConfigurationFormService { - readonly tabsContainer: TabsContainer; - readonly actionsContainer: PlaceholderContainer; - - readonly configureTask: IExecutorHandlersCollection; - readonly fillConfigTask: IExecutorHandlersCollection; - readonly prepareConfigTask: IExecutorHandlersCollection; - readonly formValidationTask: IExecutorHandlersCollection; - readonly formSubmittingTask: IExecutorHandlersCollection; - readonly formStateTask: IExecutorHandlersCollection; - - constructor(private readonly notificationService: NotificationService) { - this.tabsContainer = new TabsContainer('Identity Provider settings'); - this.actionsContainer = new PlaceholderContainer(); - this.configureTask = new ExecutorHandlersCollection(); - this.fillConfigTask = new ExecutorHandlersCollection(); - this.prepareConfigTask = new ExecutorHandlersCollection(); - this.formSubmittingTask = new ExecutorHandlersCollection(); - this.formValidationTask = new ExecutorHandlersCollection(); - this.formStateTask = new ExecutorHandlersCollection(); - - this.formSubmittingTask.before(this.formValidationTask).before(this.prepareConfigTask); - - this.formStateTask.before(this.prepareConfigTask, state => ({ state, submitType: 'submit' })); - - this.formSubmittingTask.addPostHandler(this.showSubmittingStatusMessage); - this.formValidationTask.addPostHandler(this.ensureValidation); - - this.actionsContainer.add(AuthConfigurationFormBaseActions); - } - - configurationValidationContext = (): IConfigurationFormValidation => ({ - valid: true, - messages: [], - info(message: string) { - this.messages.push(message); - }, - error(message: string) { - this.messages.push(message); - this.valid = false; - }, - }); - - configurationStatusContext = (): IConfigurationFormStatus => ({ - saved: true, - messages: [], - exception: null, - info(message: string) { - this.messages.push(message); - }, - error(message: string, exception: Error | null = null) { - this.messages.push(message); - this.saved = false; - this.exception = exception; - }, - }); - - private readonly showSubmittingStatusMessage: IExecutorHandler = (data, contexts) => { - const status = contexts.getContext(this.configurationStatusContext); - - if (!status.saved) { - ExecutorInterrupter.interrupt(contexts); - } - - if (status.messages.length > 0) { - if (status.exception) { - this.notificationService.logException(status.exception, status.messages[0], status.messages.slice(1).join('\n')); - } else { - this.notificationService.notify( - { - title: status.messages[0], - message: status.messages.slice(1).join('\n'), - }, - status.saved ? ENotificationType.Success : ENotificationType.Error, - ); - } - } - }; - - private readonly ensureValidation: IExecutorHandler = (data, contexts) => { - const validation = contexts.getContext(this.configurationValidationContext); - - if (!validation.valid) { - ExecutorInterrupter.interrupt(contexts); - } - - if (validation.messages.length > 0) { - this.notificationService.notify( - { - title: - data.state.mode === 'edit' - ? 'administration_identity_providers_provider_save_error' - : 'administration_identity_providers_provider_create_error', - message: validation.messages.join('\n'), - }, - validation.valid ? ENotificationType.Info : ENotificationType.Error, - ); - } - }; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationFormState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationFormState.ts deleted file mode 100644 index f3a96b4840..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationFormState.ts +++ /dev/null @@ -1,153 +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 { computed, makeObservable, observable } from 'mobx'; - -import { Executor, IExecutionContextProvider, IExecutor } from '@cloudbeaver/core-executor'; -import type { CachedMapResource } from '@cloudbeaver/core-resource'; -import type { AdminAuthProviderConfiguration, GetAuthProviderConfigurationsQueryVariables } from '@cloudbeaver/core-sdk'; - -import type { AuthConfigurationFormService } from './AuthConfigurationFormService'; -import { authConfigurationFormConfigureContext } from './Contexts/authConfigurationFormConfigureContext'; -import { authConfigurationFormStateContext, IAuthConfigurationFormStateInfo } from './Contexts/authConfigurationFormStateContext'; -import type { AuthConfigurationFormMode, IAuthConfigurationFormState, IAuthConfigurationFormSubmitData } from './IAuthConfigurationFormProps'; - -export class AuthConfigurationFormState implements IAuthConfigurationFormState { - mode: AuthConfigurationFormMode; - config: AdminAuthProviderConfiguration; - statusMessage: string | null; - configured: boolean; - - get info(): AdminAuthProviderConfiguration | undefined { - if (!this.config.id) { - return undefined; - } - - return this.resource.get(this.config.id); - } - - get loading(): boolean { - return this.loadConfigurationTask.executing || this.submittingTask.executing; - } - - get disabled(): boolean { - return this.loading || !!this.stateInfo?.disabled || this.loadConfigurationTask.executing; - } - - get readonly(): boolean { - return false; - } - - readonly resource: CachedMapResource; - - readonly service: AuthConfigurationFormService; - readonly submittingTask: IExecutor; - - private stateInfo: IAuthConfigurationFormStateInfo | null; - private readonly loadConfigurationTask: IExecutor; - private readonly formStateTask: IExecutor; - - constructor( - service: AuthConfigurationFormService, - resource: CachedMapResource, - ) { - this.resource = resource; - this.config = { - displayName: '', - id: '', - disabled: false, - parameters: {}, - providerId: '', - description: '', - iconURL: '', - }; - - this.stateInfo = null; - this.service = service; - this.formStateTask = new Executor(this, () => true); - this.loadConfigurationTask = new Executor(this, () => true); - this.submittingTask = new Executor(); - this.statusMessage = null; - this.configured = false; - this.mode = 'create'; - - makeObservable(this, { - mode: observable, - config: observable, - statusMessage: observable, - info: computed, - readonly: computed, - }); - - this.save = this.save.bind(this); - this.loadInfo = this.loadInfo.bind(this); - this.updateFormState = this.updateFormState.bind(this); - - this.formStateTask.addCollection(service.formStateTask).addPostHandler(this.updateFormState); - - this.loadConfigurationTask - .before(service.configureTask) - .addPostHandler(this.loadInfo) - .next(service.fillConfigTask, (state, contexts) => { - const configuration = contexts.getContext(authConfigurationFormConfigureContext); - - return { - state, - updated: state.info !== configuration.info || state.config.providerId !== configuration.providerId || !this.configured, - }; - }) - .next(this.formStateTask); - } - - async load(): Promise {} - - async loadConfigurationInfo(): Promise { - await this.loadConfigurationTask.execute(this); - - return this.info; - } - - setOptions(mode: AuthConfigurationFormMode): this { - this.mode = mode; - return this; - } - - setConfig(config: AdminAuthProviderConfiguration): this { - this.config = config; - return this; - } - - async save(): Promise { - await this.submittingTask.executeScope( - { - state: this, - }, - this.service.formSubmittingTask, - ); - } - - private updateFormState(data: IAuthConfigurationFormState, contexts: IExecutionContextProvider): void { - const context = contexts.getContext(authConfigurationFormStateContext); - - this.statusMessage = context.statusMessage; - - this.stateInfo = context; - this.configured = true; - } - - private async loadInfo(data: IAuthConfigurationFormState, contexts: IExecutionContextProvider) { - if (!data.config.id) { - return; - } - - if (!this.resource.has(data.config.id)) { - return; - } - - await this.resource.load(data.config.id); - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsAdministration.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsAdministration.tsx deleted file mode 100644 index 372df224ef..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsAdministration.tsx +++ /dev/null @@ -1,90 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { ADMINISTRATION_TOOLS_PANEL_STYLES, AdministrationItemContentComponent } from '@cloudbeaver/core-administration'; -import { ColoredContainer, Container, Group, Loader, ToolsAction, ToolsPanel, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; - -import { AuthConfigurationsTable } from './AuthConfigurationsTable/AuthConfigurationsTable'; -import { useConfigurationsTable } from './AuthConfigurationsTable/useConfigurationsTable'; -import { CreateAuthConfiguration } from './CreateAuthConfiguration'; -import { CreateAuthConfigurationService } from './CreateAuthConfigurationService'; - -const loaderStyle = css` - ExceptionMessage { - padding: 24px; - } -`; - -const styles = css` - ToolsPanel { - border-bottom: none; - } -`; - -export const AuthConfigurationsAdministration: AdministrationItemContentComponent = observer(function AuthConfigurationsAdministration({ sub }) { - const translate = useTranslate(); - const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES); - const service = useService(CreateAuthConfigurationService); - - const table = useConfigurationsTable(); - - return styled(style)( - - - - - {translate('ui_add')} - - - {translate('ui_refresh')} - - - {translate('ui_delete')} - - - - - {sub && ( - - - - )} - - - - - - - , - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsAdministrationNavService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsAdministrationNavService.ts deleted file mode 100644 index 674f416f28..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsAdministrationNavService.ts +++ /dev/null @@ -1,22 +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 { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { injectable } from '@cloudbeaver/core-di'; - -@injectable() -export class AuthConfigurationsAdministrationNavService { - constructor(private readonly administrationScreenService: AdministrationScreenService) {} - - navToRoot(): void { - this.administrationScreenService.navigateToItem('auth-configurations'); - } - - navToCreate(): void { - this.administrationScreenService.navigateToItemSub('auth-configurations', 'create'); - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsAdministrationService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsAdministrationService.ts deleted file mode 100644 index 674d601e59..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsAdministrationService.ts +++ /dev/null @@ -1,91 +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 React from 'react'; - -import { AdministrationItemService, AdministrationItemType } from '@cloudbeaver/core-administration'; -import { AuthConfigurationsResource, AuthProviderService, AuthProvidersResource } from '@cloudbeaver/core-authentication'; -import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import type { AdminAuthProviderConfiguration } from '@cloudbeaver/core-sdk'; - -import { CreateAuthConfigurationService } from './CreateAuthConfigurationService'; - -const AuthConfigurationsAdministration = React.lazy(async () => { - const { AuthConfigurationsAdministration } = await import('./AuthConfigurationsAdministration'); - return { default: AuthConfigurationsAdministration }; -}); -const AuthConfigurationsDrawerItem = React.lazy(async () => { - const { AuthConfigurationsDrawerItem } = await import('./AuthConfigurationsDrawerItem'); - return { default: AuthConfigurationsDrawerItem }; -}); -const IdentityProvidersServiceLink = React.lazy(async () => { - const { IdentityProvidersServiceLink } = await import('./IdentityProvidersServiceLink'); - return { default: IdentityProvidersServiceLink }; -}); - -export interface IAuthConfigurationDetailsPlaceholderProps { - configuration: AdminAuthProviderConfiguration; -} - -@injectable() -export class AuthConfigurationsAdministrationService extends Bootstrap { - readonly configurationDetailsPlaceholder = new PlaceholderContainer(); - - constructor( - private readonly administrationItemService: AdministrationItemService, - private readonly notificationService: NotificationService, - private readonly authProvidersResource: AuthProvidersResource, - private readonly authConfigurationsResource: AuthConfigurationsResource, - private readonly createConfigurationService: CreateAuthConfigurationService, - private readonly authProviderService: AuthProviderService, - ) { - super(); - } - - register(): void { - this.authProviderService.addServiceDescriptionLink({ - isSupported: provider => provider.configurable, - description: () => IdentityProvidersServiceLink, - }); - this.administrationItemService.create({ - name: 'auth-configurations', - type: AdministrationItemType.Administration, - order: 5, - configurationWizardOptions: { - description: 'administration_identity_providers_wizard_description', - }, - sub: [ - { - name: 'create', - onActivate: () => this.createConfigurationService.fillData(), - }, - ], - isHidden: () => !this.authProvidersResource.values.some(provider => provider.configurable), - getContentComponent: () => AuthConfigurationsAdministration, - getDrawerComponent: () => AuthConfigurationsDrawerItem, - onActivate: this.loadConfigurations.bind(this), - onDeActivate: (configurationWizard, outside) => { - if (outside) { - this.authConfigurationsResource.cleanNewFlags(); - } - }, - }); - } - - load(): void | Promise {} - - private async loadConfigurations() { - try { - await this.authConfigurationsResource.load(CachedMapAllKey); - } catch (exception: any) { - this.notificationService.logException(exception, 'Error occurred while loading configurations'); - } - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsDrawerItem.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsDrawerItem.tsx deleted file mode 100644 index 6cdc4dca00..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsDrawerItem.tsx +++ /dev/null @@ -1,28 +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 styled from 'reshadow'; - -import type { AdministrationItemDrawerProps } from '@cloudbeaver/core-administration'; -import { Translate, useStyles } from '@cloudbeaver/core-blocks'; -import { Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; - -export const AuthConfigurationsDrawerItem: React.FC = function AuthConfigurationsDrawerItem({ - item, - onSelect, - style, - disabled, -}) { - return styled(useStyles(style))( - onSelect(item.name)}> - - - - - , - ); -}; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfiguration.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfiguration.tsx deleted file mode 100644 index afccbe1df4..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfiguration.tsx +++ /dev/null @@ -1,84 +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 { observer } from 'mobx-react-lite'; -import styled, { css, use } from 'reshadow'; - -import { AuthProvidersResource } from '@cloudbeaver/core-authentication'; -import { - FieldCheckbox, - Loader, - Placeholder, - StaticImage, - TableColumnValue, - TableItem, - TableItemExpand, - TableItemSelect, - useResource, -} from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import type { AdminAuthProviderConfiguration } from '@cloudbeaver/core-sdk'; - -import { AuthConfigurationsAdministrationService } from '../AuthConfigurationsAdministrationService'; -import { AuthConfigurationEdit } from './AuthConfigurationEdit'; - -const styles = css` - StaticImage { - display: flex; - width: 24px; - - &:not(:last-child) { - margin-right: 16px; - } - } - TableColumnValue[expand] { - cursor: pointer; - } - TableColumnValue[|gap] { - gap: 16px; - } -`; - -interface Props { - configuration: AdminAuthProviderConfiguration; -} - -export const AuthConfiguration = observer(function AuthConfiguration({ configuration }) { - const service = useService(AuthConfigurationsAdministrationService); - const resource = useResource(AuthConfiguration, AuthProvidersResource, configuration.providerId); - - const icon = configuration.iconURL || resource.data?.icon; - - return styled(styles)( - - - - - - - - - - - - {configuration.displayName} - - {configuration.providerId} - - {configuration.description || ''} - - - - - - - - - - , - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfigurationEdit.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfigurationEdit.tsx deleted file mode 100644 index 9f6dbd2448..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfigurationEdit.tsx +++ /dev/null @@ -1,51 +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 { observer } from 'mobx-react-lite'; -import { useCallback, useContext } from 'react'; -import styled, { css } from 'reshadow'; - -import { AuthConfigurationsResource } from '@cloudbeaver/core-authentication'; -import { TableContext, useStyles } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; - -import { AuthConfigurationForm } from '../AuthConfigurationForm'; -import { useAuthConfigurationFormState } from '../useAuthConfigurationFormState'; - -const styles = css` - box { - composes: theme-background-secondary theme-text-on-secondary from global; - box-sizing: border-box; - padding-bottom: 24px; - display: flex; - flex-direction: column; - height: 664px; - } -`; - -interface Props { - item: string; -} - -export const AuthConfigurationEdit = observer(function AuthConfigurationEdit({ item }) { - const resource = useService(AuthConfigurationsResource); - const tableContext = useContext(TableContext); - - const collapse = useCallback(() => { - tableContext?.setItemExpand(item, false); - }, [tableContext, item]); - - const data = useAuthConfigurationFormState(resource, state => state.setOptions('edit')); - - data.config.id = item; - - return styled(useStyles(styles))( - - - , - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfigurationsTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfigurationsTable.tsx deleted file mode 100644 index afa50ea602..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfigurationsTable.tsx +++ /dev/null @@ -1,46 +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 { observer } from 'mobx-react-lite'; - -import { Table, TableBody, TableColumnHeader, TableHeader, TableSelect, useTranslate } from '@cloudbeaver/core-blocks'; -import type { AdminAuthProviderConfiguration } from '@cloudbeaver/core-sdk'; - -import { AuthConfiguration } from './AuthConfiguration'; - -interface Props { - configurations: AdminAuthProviderConfiguration[]; - selectedItems: Map; - expandedItems: Map; -} - -export const AuthConfigurationsTable = observer(function AuthConfigurationsTable({ configurations, selectedItems, expandedItems }) { - const translate = useTranslate(); - const keys = configurations.map(configuration => configuration.id); - - return ( - - - - - - - - {translate('administration_identity_providers_provider_configuration_name')} - {translate('administration_identity_providers_provider')} - {translate('administration_identity_providers_provider_configuration_description')} - {translate('administration_identity_providers_provider_configuration_disabled')} - - - - {configurations.map(configuration => ( - - ))} - -
- ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/useConfigurationsTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/useConfigurationsTable.tsx deleted file mode 100644 index ebd47d5564..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/useConfigurationsTable.tsx +++ /dev/null @@ -1,100 +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 { computed, observable } from 'mobx'; - -import { AuthConfigurationsResource, compareAuthConfigurations } from '@cloudbeaver/core-authentication'; -import { ConfirmationDialogDelete, TableState, useObservableRef, useTranslate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { CachedMapAllKey, resourceKeyList } from '@cloudbeaver/core-resource'; -import type { AdminAuthProviderConfiguration } from '@cloudbeaver/core-sdk'; - -interface State { - tableState: TableState; - processing: boolean; - configurations: AdminAuthProviderConfiguration[]; - update: () => Promise; - delete: () => Promise; -} - -export function useConfigurationsTable(): Readonly { - const notificationService = useService(NotificationService); - const dialogService = useService(CommonDialogService); - const resource = useService(AuthConfigurationsResource); - - const translate = useTranslate(); - - return useObservableRef( - () => ({ - tableState: new TableState(), - processing: false, - get configurations() { - return resource.values.slice().sort(compareAuthConfigurations); - }, - async update() { - if (this.processing) { - return; - } - - try { - this.processing = true; - await resource.refresh(CachedMapAllKey); - notificationService.logSuccess({ title: 'administration_identity_providers_configuration_list_update_success' }); - } catch (exception: any) { - notificationService.logException(exception, 'administration_identity_providers_configuration_list_update_fail'); - } finally { - this.processing = false; - } - }, - async delete() { - if (this.processing) { - return; - } - - const deletionList = this.tableState.selectedList; - - if (deletionList.length === 0) { - return; - } - - const configurationNames = deletionList.map(id => resource.get(id)?.displayName).filter(Boolean); - const nameList = configurationNames.map(name => `"${name}"`).join(', '); - const message = `${translate('administration_identity_providers_delete_confirmation')}${nameList}. ${translate('ui_are_you_sure')}`; - - const result = await dialogService.open(ConfirmationDialogDelete, { - title: 'ui_data_delete_confirmation', - message, - confirmActionText: 'ui_delete', - }); - - if (result === DialogueStateResult.Rejected) { - return; - } - - try { - this.processing = true; - await resource.deleteConfiguration(resourceKeyList(deletionList)); - - this.tableState.unselect(); - this.tableState.collapse(deletionList); - } catch (exception: any) { - notificationService.logException(exception, 'Configurations delete failed'); - } finally { - this.processing = false; - } - }, - }), - { - processing: observable.ref, - configurations: computed, - }, - false, - ['update', 'delete'], - ); -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Contexts/authConfigurationContext.ts b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Contexts/authConfigurationContext.ts deleted file mode 100644 index 680f7fb400..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Contexts/authConfigurationContext.ts +++ /dev/null @@ -1,20 +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 type { AdminAuthProviderConfiguration } from '@cloudbeaver/core-sdk'; - -export function authConfigurationContext(): AdminAuthProviderConfiguration { - return { - id: '', - providerId: '', - displayName: '', - disabled: false, - parameters: {}, - description: '', - iconURL: '', - }; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Contexts/authConfigurationFormConfigureContext.ts b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Contexts/authConfigurationFormConfigureContext.ts deleted file mode 100644 index a800cd4593..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Contexts/authConfigurationFormConfigureContext.ts +++ /dev/null @@ -1,26 +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 type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import type { AdminAuthProviderConfiguration } from '@cloudbeaver/core-sdk'; - -import type { IAuthConfigurationFormState } from '../IAuthConfigurationFormProps'; - -export interface IAuthConfigurationFormConfigureContext { - readonly providerId: string | undefined; - readonly info: AdminAuthProviderConfiguration | undefined; -} - -export function authConfigurationFormConfigureContext( - contexts: IExecutionContextProvider, - state: IAuthConfigurationFormState, -): IAuthConfigurationFormConfigureContext { - return { - info: state.info, - providerId: state.config.providerId, - }; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Contexts/authConfigurationFormStateContext.ts b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Contexts/authConfigurationFormStateContext.ts deleted file mode 100644 index 5e69f39730..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Contexts/authConfigurationFormStateContext.ts +++ /dev/null @@ -1,30 +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. - */ - -export interface IAuthConfigurationFormStateInfo { - edited: boolean; - disabled: boolean; - readonly: boolean; - statusMessage: string | null; -} - -export interface IAuthConfigurationFormStateContext extends IAuthConfigurationFormStateInfo { - setStatusMessage: (message: string | null) => void; -} - -export function authConfigurationFormStateContext(): IAuthConfigurationFormStateContext { - return { - edited: false, - disabled: false, - readonly: false, - statusMessage: null, - setStatusMessage(message: string | null) { - this.statusMessage = message; - }, - }; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/CreateAuthConfiguration.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/CreateAuthConfiguration.tsx deleted file mode 100644 index 817a4de3bb..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/CreateAuthConfiguration.tsx +++ /dev/null @@ -1,68 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { IconButton, Translate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; - -import { AuthConfigurationForm } from './AuthConfigurationForm'; -import { CreateAuthConfigurationService } from './CreateAuthConfigurationService'; - -const styles = css` - configuration-create { - display: flex; - flex-direction: column; - height: 660px; - overflow: hidden; - } - - title-bar { - composes: theme-border-color-background theme-typography--headline6 from global; - box-sizing: border-box; - padding: 16px 24px; - align-items: center; - display: flex; - font-weight: 400; - flex: auto 0 0; - } - - configuration-create-content { - composes: theme-background-secondary theme-text-on-secondary from global; - position: relative; - display: flex; - flex-direction: column; - flex: 1; - overflow: auto; - } - - fill { - flex: 1; - } -`; - -export const CreateAuthConfiguration: React.FC = observer(function CreateAuthConfiguration() { - const service = useService(CreateAuthConfigurationService); - - if (!service.data) { - return null; - } - - return styled(styles)( - - - - - - - - - - , - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/CreateAuthConfigurationService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/CreateAuthConfigurationService.ts deleted file mode 100644 index 6c7fcc893d..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/CreateAuthConfigurationService.ts +++ /dev/null @@ -1,50 +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 { makeObservable, observable } from 'mobx'; - -import { AuthConfigurationsResource } from '@cloudbeaver/core-authentication'; -import { injectable } from '@cloudbeaver/core-di'; - -import { AuthConfigurationFormService } from './AuthConfigurationFormService'; -import { AuthConfigurationFormState } from './AuthConfigurationFormState'; -import { AuthConfigurationsAdministrationNavService } from './AuthConfigurationsAdministrationNavService'; -import type { IAuthConfigurationFormState } from './IAuthConfigurationFormProps'; - -@injectable() -export class CreateAuthConfigurationService { - disabled = false; - data: IAuthConfigurationFormState | null; - - constructor( - private readonly authConfigurationsAdministrationNavService: AuthConfigurationsAdministrationNavService, - private readonly authConfigurationFormService: AuthConfigurationFormService, - private readonly authConfigurationsResource: AuthConfigurationsResource, - ) { - this.data = null; - - this.cancelCreate = this.cancelCreate.bind(this); - this.create = this.create.bind(this); - - makeObservable(this, { - data: observable, - disabled: observable, - }); - } - - cancelCreate(): void { - this.authConfigurationsAdministrationNavService.navToRoot(); - } - - fillData(): void { - this.data = new AuthConfigurationFormState(this.authConfigurationFormService, this.authConfigurationsResource); - } - - create(): void { - this.authConfigurationsAdministrationNavService.navToCreate(); - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/IAuthConfigurationFormProps.ts b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/IAuthConfigurationFormProps.ts deleted file mode 100644 index 5eb125396e..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/IAuthConfigurationFormProps.ts +++ /dev/null @@ -1,45 +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 type { IExecutorHandlersCollection } from '@cloudbeaver/core-executor'; -import type { CachedMapResource } from '@cloudbeaver/core-resource'; -import type { AdminAuthProviderConfiguration, GetAuthProviderConfigurationsQueryVariables } from '@cloudbeaver/core-sdk'; - -export type AuthConfigurationFormMode = 'edit' | 'create'; - -export interface IAuthConfigurationFormState { - mode: AuthConfigurationFormMode; - config: AdminAuthProviderConfiguration; - - readonly info: AdminAuthProviderConfiguration | undefined; - readonly statusMessage: string | null; - readonly disabled: boolean; - readonly readonly: boolean; - readonly loading: boolean; - - readonly submittingTask: IExecutorHandlersCollection; - readonly resource: CachedMapResource; - - readonly load: () => Promise; - readonly loadConfigurationInfo: () => Promise; - readonly save: () => Promise; - readonly setOptions: (mode: AuthConfigurationFormMode) => this; -} - -export interface IAuthConfigurationFormProps { - state: IAuthConfigurationFormState; - onCancel?: () => void; -} - -export interface IAuthConfigurationFormFillConfigData { - updated: boolean; - state: IAuthConfigurationFormState; -} - -export interface IAuthConfigurationFormSubmitData { - state: IAuthConfigurationFormState; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/IdentityProvidersServiceLink.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/IdentityProvidersServiceLink.tsx deleted file mode 100644 index 13ed6b0e38..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/IdentityProvidersServiceLink.tsx +++ /dev/null @@ -1,26 +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 type { ServiceDescriptionComponent } from '@cloudbeaver/core-authentication'; -import { Link, Translate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; - -import { AuthConfigurationsAdministrationNavService } from './AuthConfigurationsAdministrationNavService'; - -export const IdentityProvidersServiceLink: ServiceDescriptionComponent = function IdentityProvidersServiceLink({ configurationWizard }) { - const authConfigurationsAdministrationNavService = useService(AuthConfigurationsAdministrationNavService); - - if (configurationWizard) { - return null; - } - - return ( - authConfigurationsAdministrationNavService.navToRoot()}> - - - ); -}; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Options/AuthConfigurationOptions.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Options/AuthConfigurationOptions.tsx deleted file mode 100644 index 9141d2c1a2..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Options/AuthConfigurationOptions.tsx +++ /dev/null @@ -1,184 +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 { observer } from 'mobx-react-lite'; -import { useCallback, useRef } from 'react'; -import styled, { css } from 'reshadow'; - -import { AuthConfigurationParametersResource, AuthProvidersResource } from '@cloudbeaver/core-authentication'; -import { - ColoredContainer, - Combobox, - FieldCheckbox, - Form, - Group, - GroupTitle, - InputField, - Link, - ObjectPropertyInfoForm, - Textarea, - useClipboard, - useObjectPropertyCategories, - useResource, - useStyles, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import type { AuthProviderConfigurationParametersFragment } from '@cloudbeaver/core-sdk'; -import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; - -import type { IAuthConfigurationFormProps } from '../IAuthConfigurationFormProps'; - -const styles = css` - Form { - flex: 1; - overflow: auto; - } -`; - -const emptyArray: AuthProviderConfigurationParametersFragment[] = []; - -export const AuthConfigurationOptions: TabContainerPanelComponent = observer(function AuthConfigurationOptions({ - state, -}) { - const formRef = useRef(null); - - const translate = useTranslate(); - const copy = useClipboard(); - const style = useStyles(styles); - - const providers = useResource(AuthConfigurationOptions, AuthProvidersResource, CachedMapAllKey); - const parameters = useResource(AuthConfigurationOptions, AuthConfigurationParametersResource, state.config.providerId || null); - - const { categories, isUncategorizedExists } = useObjectPropertyCategories(parameters.data ?? emptyArray); - - const edit = state.mode === 'edit'; - - const handleProviderSelect = useCallback(() => { - state.config.parameters = {}; - }, [state]); - - return styled(style)( -
- - - provider.id} - valueSelector={provider => provider.label} - iconSelector={provider => provider.icon} - titleSelector={provider => provider.description} - placeholder={translate('administration_identity_providers_choose_provider_placeholder')} - readOnly={state.readonly || edit} - disabled={state.disabled} - required - tiny - fill - onSelect={handleProviderSelect} - > - {translate('administration_identity_providers_provider')} - - - {translate('administration_identity_providers_provider_id')} - - - {translate('administration_identity_providers_provider_configuration_name')} - - - - {translate('administration_identity_providers_provider_configuration_icon_url')} - - - {translate('administration_identity_providers_provider_configuration_disabled')} - - - {parameters.isLoaded() && parameters.data && ( - <> - {isUncategorizedExists && ( - - {translate('administration_identity_providers_provider_configuration_parameters')} - - - )} - {categories.map(category => ( - - {category} - - - ))} - - )} - {(state.config.metadataLink || state.config.signInLink || state.config.signOutLink) && ( - - {translate('administration_identity_providers_provider_configuration_links')} - copy(state.config.signInLink!, true)} - > - Sign in - - copy(state.config.signOutLink!, true)} - > - Sign out - - copy(state.config.redirectLink!, true)} - > - Redirect - - {state.config.metadataLink && ( - - {translate('administration_identity_providers_provider_configuration_links_metadata')} - - )} - - )} - -
, - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Options/AuthConfigurationOptionsTabService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Options/AuthConfigurationOptionsTabService.ts deleted file mode 100644 index fe311ba7de..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Options/AuthConfigurationOptionsTabService.ts +++ /dev/null @@ -1,181 +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 React from 'react'; - -import { AuthConfigurationsResource, AuthProvidersResource } from '@cloudbeaver/core-authentication'; -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { getUniqueName } from '@cloudbeaver/core-utils'; - -import { AuthConfigurationFormService } from '../AuthConfigurationFormService'; -import { authConfigurationContext } from '../Contexts/authConfigurationContext'; -import type { - IAuthConfigurationFormFillConfigData, - IAuthConfigurationFormState, - IAuthConfigurationFormSubmitData, -} from '../IAuthConfigurationFormProps'; - -const AuthConfigurationOptions = React.lazy(async () => { - const { AuthConfigurationOptions } = await import('./AuthConfigurationOptions'); - return { default: AuthConfigurationOptions }; -}); - -@injectable() -export class AuthConfigurationOptionsTabService extends Bootstrap { - constructor( - private readonly authConfigurationFormService: AuthConfigurationFormService, - private readonly authConfigurationsResource: AuthConfigurationsResource, - private readonly authProvidersResource: AuthProvidersResource, - ) { - super(); - } - - register(): void { - this.authConfigurationFormService.tabsContainer.add({ - key: 'options', - name: 'ui_options', - order: 1, - panel: () => AuthConfigurationOptions, - }); - - this.authConfigurationFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this)); - - this.authConfigurationFormService.formValidationTask.addHandler(this.validate.bind(this)); - - this.authConfigurationFormService.formSubmittingTask.addHandler(this.save.bind(this)); - - this.authConfigurationFormService.fillConfigTask.addHandler(this.fillConfig.bind(this)); - } - - load(): void {} - - private async prepareConfig({ state }: IAuthConfigurationFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(authConfigurationContext); - - config.id = state.config.id; - config.providerId = state.config.providerId; - config.disabled = state.config.disabled; - config.displayName = state.config.displayName.trim(); - - if (state.mode === 'create') { - const configurationNames = this.authConfigurationsResource.values.map(configuration => configuration.displayName); - config.displayName = getUniqueName(config.displayName, configurationNames); - } - - if (Object.keys(state.config.parameters).length) { - config.parameters = state.config.parameters; - } - - if (state.config.description) { - config.description = state.config.description; - } - - if (state.config.iconURL) { - config.iconURL = state.config.iconURL; - } - } - - private async validate({ state }: IAuthConfigurationFormSubmitData, contexts: IExecutionContextProvider) { - const validation = contexts.getContext(this.authConfigurationFormService.configurationValidationContext); - - if (!state.config.displayName.trim()) { - validation.error("Field 'Name' can't be empty"); - } - - if (state.mode === 'create') { - if (!state.config.providerId) { - validation.error("Field 'Provider' can't be empty"); - } - - if (!state.config.id.trim()) { - validation.error("Field 'ID' can't be empty"); - } - - if (this.authConfigurationsResource.has(state.config.id)) { - validation.error(`A configuration with ID "${state.config.id}" already exists`); - } - } - } - - private async save({ state }: IAuthConfigurationFormSubmitData, contexts: IExecutionContextProvider) { - const status = contexts.getContext(this.authConfigurationFormService.configurationStatusContext); - const config = contexts.getContext(authConfigurationContext); - - try { - const configuration = await this.authConfigurationsResource.saveConfiguration(config); - - if (state.mode === 'create') { - status.info('Configuration created'); - status.info(configuration.displayName); - } else { - status.info('Configuration updated'); - status.info(configuration.displayName); - } - } catch (exception: any) { - status.error('connections_connection_create_fail', exception); - } - } - - private async setDefaults(state: IAuthConfigurationFormState) { - if (state.mode === 'create') { - await this.authProvidersResource.load(CachedMapAllKey); - if (this.authProvidersResource.configurable.length > 0 && !state.config.providerId) { - state.config.providerId = this.authProvidersResource.configurable[0].id; - } - } - } - - private async fillConfig( - { state, updated }: IAuthConfigurationFormFillConfigData, - contexts: IExecutionContextProvider, - ) { - if (!updated) { - return; - } - - if (!state.info) { - await this.setDefaults(state); - return; - } - - if (state.info.id) { - state.config.id = state.info.id; - } - if (state.info.providerId) { - state.config.providerId = state.info.providerId; - } - if (state.info.displayName) { - state.config.displayName = state.info.displayName; - } - if (state.info.disabled !== undefined) { - state.config.disabled = state.info.disabled; - } - if (state.info.iconURL) { - state.config.iconURL = state.info.iconURL; - } - if (state.info.description) { - state.config.description = state.info.description; - } - if (state.info.metadataLink) { - state.config.metadataLink = state.info.metadataLink; - } - if (state.info.signInLink) { - state.config.signInLink = state.info.signInLink; - } - if (state.info.signOutLink) { - state.config.signOutLink = state.info.signOutLink; - } - if (state.info.redirectLink) { - state.config.redirectLink = state.info.redirectLink; - } - if (state.info.parameters) { - state.config.parameters = { ...state.info.parameters }; - } - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/useAuthConfigurationFormState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/useAuthConfigurationFormState.ts deleted file mode 100644 index 0aa0fb427b..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/useAuthConfigurationFormState.ts +++ /dev/null @@ -1,32 +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 { useState } from 'react'; - -import { useService } from '@cloudbeaver/core-di'; -import type { CachedMapResource } from '@cloudbeaver/core-resource'; -import type { AdminAuthProviderConfiguration, GetAuthProviderConfigurationsQueryVariables } from '@cloudbeaver/core-sdk'; - -import { AuthConfigurationFormService } from './AuthConfigurationFormService'; -import { AuthConfigurationFormState } from './AuthConfigurationFormState'; -import type { IAuthConfigurationFormState } from './IAuthConfigurationFormProps'; - -export function useAuthConfigurationFormState( - resource: CachedMapResource, - configure?: (state: IAuthConfigurationFormState) => any, -): IAuthConfigurationFormState { - const service = useService(AuthConfigurationFormService); - const [state] = useState(() => { - const state = new AuthConfigurationFormState(service, resource); - configure?.(state); - - state.load(); - return state; - }); - - return state; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx index 8810379ffc..d093630335 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,17 +10,18 @@ import React, { useContext } from 'react'; import { AUTH_PROVIDER_LOCAL_ID, - AuthProvider, AuthProviderService, AuthProvidersResource, AuthSettingsService, + sortProvider, } from '@cloudbeaver/core-authentication'; -import { FormContext, Group, GroupTitle, PlaceholderComponent, Switch, useExecutor, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { FormContext, Group, GroupTitle, type PlaceholderComponent, Switch, useExecutor, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { isDefined } from '@dbeaver/js-helpers'; import type { IConfigurationPlaceholderProps } from '@cloudbeaver/plugin-administration'; -import { ServerConfigurationAdminForm } from './ServerConfigurationAdminForm'; +import { ServerConfigurationAdminForm } from './ServerConfigurationAdminForm.js'; export const AuthenticationProviders: PlaceholderComponent = observer(function AuthenticationProviders({ state: { serverConfig }, @@ -36,19 +37,29 @@ export const AuthenticationProviders: PlaceholderComponent((provider): provider is AuthProvider => { - if (configurationWizard && (provider?.configurable || provider?.private)) { - return false; - } + const localProvider = providers.resource.get(AUTH_PROVIDER_LOCAL_ID); + const providerList = providers.data + .filter(isDefined) + .filter(provider => { + if (provider.private) { + return false; + } + + if (configurationWizard) { + const disabledByFeature = provider.requiredFeatures.some(feat => !serverConfig.enabledFeatures?.includes(feat)); + + if (provider.configurable || disabledByFeature || provider.authHidden) { + return false; + } + } - return true; - }); + return true; + }) + .sort(sortProvider); - const localProvider = providers.resource.get(AUTH_PROVIDER_LOCAL_ID); - const primaryProvider = providers.resource.get(providers.resource.getPrimary()); - const externalAuthentication = localProvider === undefined && providerList.length === 1; + const externalAuthentication = providerList.length === 0; const authenticationDisabled = serverConfig.enabledAuthProviders?.length === 0; - const isAnonymousAccessDisabled = authSettingsService.settings.getValue('disableAnonymousAccess'); + const isAnonymousAccessDisabled = authSettingsService.disableAnonymousAccess; useExecutor({ executor: formContext.onChange, @@ -57,8 +68,6 @@ export const AuthenticationProviders: PlaceholderComponent { const links = authProviderService.getServiceDescriptionLinks(provider); - let disabled = provider.requiredFeatures.some(feat => !serverConfig.enabledFeatures?.includes(feat)); + const disabled = provider.requiredFeatures.some(feat => !serverConfig.enabledFeatures?.includes(feat)); const tooltip = disabled ? `Following services need to be enabled: "${provider.requiredFeatures.join(', ')}"` : ''; - if ( - !localProvider && - primaryProvider?.id === provider.id && - serverConfig.enabledAuthProviders?.length === 1 && - serverConfig.enabledAuthProviders.includes(provider.id) - ) { - disabled = true; - } - - if (provider.private || (configurationWizard && (disabled || provider.id !== AUTH_PROVIDER_LOCAL_ID))) { - return null; - } - return ( (function ServerConfigurationAdminForm({ serverConfig }) { const translate = useTranslate(); + const passwordValidationRef = usePasswordValidation(); + + const passwordRepeatRef = useCustomInputValidation(value => { + if (!isValuesEqual(value, serverConfig.adminPassword, null)) { + return translate('authentication_user_passwords_not_match'); + } + return null; + }); return ( {translate('administration_configuration_wizard_configuration_admin')} - + {translate('administration_configuration_wizard_configuration_admin_name')} - + {translate('administration_configuration_wizard_configuration_admin_password')} + {/* @ts-ignore We need adminPasswordRepeat in state to validate it on navigation, but we don't have this field in serverConfig */} + + {translate('authentication_user_password_repeat')} + ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap.ts b/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap.ts deleted file mode 100644 index 056aff4f77..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap.ts +++ /dev/null @@ -1,87 +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 { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource } from '@cloudbeaver/core-authentication'; -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { - ILoadConfigData, - IServerConfigSaveData, - ServerConfigurationService, - serverConfigValidationContext, -} from '@cloudbeaver/plugin-administration'; - -@injectable() -export class ServerConfigurationAuthenticationBootstrap extends Bootstrap { - constructor( - private readonly administrationScreenService: AdministrationScreenService, - private readonly serverConfigurationService: ServerConfigurationService, - private readonly authProvidersResource: AuthProvidersResource, - private readonly serverConfigResource: ServerConfigResource, - private readonly notificationService: NotificationService, - ) { - super(); - } - - register(): void { - this.serverConfigurationService.validationTask.addHandler(this.validateForm); - this.serverConfigurationService.loadConfigTask.addHandler(this.loadServerConfig); - } - - load(): void {} - - private readonly loadServerConfig: IExecutorHandler = async (data, contexts) => { - if (!data.reset) { - return; - } - - try { - const config = await this.serverConfigResource.load(); - - if (!config) { - return; - } - - if (this.administrationScreenService.isConfigurationMode) { - await this.authProvidersResource.load(CachedMapAllKey); - if (this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID)) { - data.state.serverConfig.adminName = 'cbadmin'; - data.state.serverConfig.adminPassword = ''; - } - } else { - data.state.serverConfig.adminName = undefined; - data.state.serverConfig.adminPassword = undefined; - } - - data.state.serverConfig.anonymousAccessEnabled = config.anonymousAccessEnabled; - data.state.serverConfig.enabledAuthProviders = [...config.enabledAuthProviders]; - data.state.serverConfig.enabledFeatures = [...config.enabledFeatures]; - } catch (exception: any) { - ExecutorInterrupter.interrupt(contexts); - this.notificationService.logException(exception, "Can't load server configuration"); - } - }; - - private readonly validateForm: IExecutorHandler = async (data, contexts) => { - await this.authProvidersResource.load(CachedMapAllKey); - const administratorPresented = data.configurationWizard && this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID); - - if (!administratorPresented) { - return; - } - - const validation = contexts.getContext(serverConfigValidationContext); - - if (!data.state.serverConfig.adminName || data.state.serverConfig.adminName.length < 6 || !data.state.serverConfig.adminPassword) { - validation.invalidate(); - } - }; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/ADMINISTRATION_ITEM_USER_CREATE_PARAM.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/ADMINISTRATION_ITEM_USER_CREATE_PARAM.ts index c631c31fa7..1dd441431d 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/ADMINISTRATION_ITEM_USER_CREATE_PARAM.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/ADMINISTRATION_ITEM_USER_CREATE_PARAM.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Contexts/teamContext.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Contexts/teamContext.ts deleted file mode 100644 index 25f09ee637..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Contexts/teamContext.ts +++ /dev/null @@ -1,16 +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 type { TeamInfo } from '@cloudbeaver/core-authentication'; - -export function teamContext(): TeamInfo { - return { - teamId: '', - teamPermissions: [], - metaParameters: {}, - }; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Contexts/teamFormConfigureContext.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Contexts/teamFormConfigureContext.ts deleted file mode 100644 index e4d32f83b8..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Contexts/teamFormConfigureContext.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 type { TeamInfo } from '@cloudbeaver/core-authentication'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; - -import type { ITeamFormState } from '../ITeamFormProps'; - -export interface ITeamFormConfigureContext { - readonly info: TeamInfo | undefined; -} - -export function teamFormConfigureContext(contexts: IExecutionContextProvider, state: ITeamFormState): ITeamFormConfigureContext { - return { - info: state.info, - }; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Contexts/teamFormStateContext.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Contexts/teamFormStateContext.ts deleted file mode 100644 index fe99c1416a..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Contexts/teamFormStateContext.ts +++ /dev/null @@ -1,30 +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. - */ - -export interface ITeamFormStateInfo { - edited: boolean; - disabled: boolean; - readonly: boolean; - statusMessage: string | null; -} - -export interface ITeamFormStateContext extends ITeamFormStateInfo { - setStatusMessage: (message: string | null) => void; -} - -export function teamFormStateContext(): ITeamFormStateContext { - return { - edited: false, - disabled: false, - readonly: false, - statusMessage: null, - setStatusMessage(message: string | null) { - this.statusMessage = message; - }, - }; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/CreateTeam.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/CreateTeam.tsx deleted file mode 100644 index a79b450922..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/CreateTeam.tsx +++ /dev/null @@ -1,68 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { IconButton, Translate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; - -import { CreateTeamService } from './CreateTeamService'; -import { TeamForm } from './TeamForm'; - -const styles = css` - team-create { - display: flex; - flex-direction: column; - height: 660px; - overflow: hidden; - } - - title-bar { - composes: theme-border-color-background theme-typography--headline6 from global; - box-sizing: border-box; - padding: 16px 24px; - align-items: center; - display: flex; - font-weight: 400; - flex: auto 0 0; - } - - team-create-content { - composes: theme-background-secondary theme-text-on-secondary from global; - position: relative; - display: flex; - flex-direction: column; - flex: 1; - overflow: auto; - } - - fill { - flex: 1; - } -`; - -export const CreateTeam: React.FC = observer(function CreateTeam() { - const service = useService(CreateTeamService); - - if (!service.data) { - return null; - } - - return styled(styles)( - - - - - - - - - - , - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/CreateTeamService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/CreateTeamService.ts deleted file mode 100644 index 05bf961504..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/CreateTeamService.ts +++ /dev/null @@ -1,50 +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 { makeObservable, observable } from 'mobx'; - -import { TeamsResource } from '@cloudbeaver/core-authentication'; -import { injectable } from '@cloudbeaver/core-di'; - -import type { ITeamFormState } from './ITeamFormProps'; -import { TeamFormService } from './TeamFormService'; -import { TeamFormState } from './TeamFormState'; -import { TeamsAdministrationNavService } from './TeamsAdministrationNavService'; - -@injectable() -export class CreateTeamService { - disabled = false; - data: ITeamFormState | null; - - constructor( - private readonly teamsAdministrationNavService: TeamsAdministrationNavService, - private readonly teamFormService: TeamFormService, - private readonly teamsResource: TeamsResource, - ) { - this.data = null; - - this.cancelCreate = this.cancelCreate.bind(this); - this.create = this.create.bind(this); - - makeObservable(this, { - data: observable, - disabled: observable, - }); - } - - cancelCreate(): void { - this.teamsAdministrationNavService.navToRoot(); - } - - fillData(): void { - this.data = new TeamFormState(this.teamFormService, this.teamsResource); - } - - create(): void { - this.teamsAdministrationNavService.navToCreate(); - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/ConnectionList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/ConnectionList.tsx deleted file mode 100644 index f2da8a0a04..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/ConnectionList.tsx +++ /dev/null @@ -1,124 +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 { observable } from 'mobx'; -import { observer } from 'mobx-react-lite'; -import { useCallback, useState } from 'react'; -import styled, { css } from 'reshadow'; - -import { - Button, - getComputed, - getSelectedItems, - Group, - Table, - TableBody, - TableColumnValue, - TableItem, - useObjectRef, - useStyles, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import { Connection, DBDriverResource } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; - -import { getFilteredConnections } from './getFilteredConnections'; -import { GrantedConnectionsTableHeader, IFilterState } from './GrantedConnectionsTableHeader/GrantedConnectionsTableHeader'; -import { GrantedConnectionsTableInnerHeader } from './GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader'; -import { GrantedConnectionsTableItem } from './GrantedConnectionsTableItem'; - -const styles = css` - Group { - position: relative; - } - Group, - container, - table-container { - height: 100%; - } - container { - display: flex; - flex-direction: column; - width: 100%; - } - table-container { - overflow: auto; - } - GrantedConnectionsTableHeader { - flex: 0 0 auto; - } - Table { - composes: theme-background-surface theme-text-on-surface from global; - width: 100%; - } -`; - -interface Props { - connectionList: Connection[]; - grantedSubjects: string[]; - disabled: boolean; - onGrant: (subjectIds: string[]) => void; -} - -export const ConnectionList = observer(function ConnectionList({ connectionList, grantedSubjects, disabled, onGrant }) { - const props = useObjectRef({ onGrant }); - const style = useStyles(styles); - const translate = useTranslate(); - - const driversResource = useService(DBDriverResource); - - const [selectedSubjects] = useState>(() => observable(new Map())); - const [filterState] = useState(() => observable({ filterValue: '' })); - - const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); - - const grant = useCallback(() => { - props.onGrant(getSelectedItems(selectedSubjects)); - selectedSubjects.clear(); - }, []); - - const connections = getFilteredConnections(connectionList, filterState.filterValue); - const keys = connections.map(connection => connection.id); - - return styled(style)( - - - - - - - !grantedSubjects.includes(item)} size="big"> - - - {!connections.length && filterState.filterValue && ( - - {translate('ui_search_no_result_placeholder')} - - )} - {connections.map(connection => { - const driver = driversResource.get(connection.driverId); - return ( - - ); - })} - -
-
-
-
, - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnections.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnections.tsx deleted file mode 100644 index f9d53126ba..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnections.tsx +++ /dev/null @@ -1,126 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { - ColoredContainer, - Container, - getComputed, - Group, - InfoItem, - Loader, - TextPlaceholder, - useAutoLoad, - useResource, - useStyles, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import { Connection, ConnectionInfoProjectKey, ConnectionInfoResource, DBDriverResource, isCloudConnection } from '@cloudbeaver/core-connections'; -import type { TLocalizationToken } from '@cloudbeaver/core-localization'; -import { isGlobalProject, ProjectInfo, ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; - -import type { ITeamFormProps } from '../ITeamFormProps'; -import { ConnectionList } from './ConnectionList'; -import { GrantedConnectionList } from './GrantedConnectionsList'; -import { useGrantedConnections } from './useGrantedConnections'; - -const styles = css` - ColoredContainer { - flex: 1; - height: 100%; - box-sizing: border-box; - } - Group { - max-height: 100%; - position: relative; - overflow: auto !important; - } - Loader { - z-index: 2; - } -`; - -export const GrantedConnections: TabContainerPanelComponent = observer(function GrantedConnections({ tabId, state: formState }) { - const style = useStyles(styles); - const translate = useTranslate(); - - const state = useGrantedConnections(formState.config, formState.mode); - const { selected } = useTab(tabId); - const loaded = state.state.loaded; - - const projects = useResource(GrantedConnections, ProjectInfoResource, CachedMapAllKey); - - const globalConnectionsKey = ConnectionInfoProjectKey( - ...(projects.data as Array).filter(isGlobalProject).map(project => project.id), - ); - - useResource(GrantedConnections, DBDriverResource, CachedMapAllKey, { active: selected }); - - const connectionsLoader = useResource(GrantedConnections, ConnectionInfoResource, globalConnectionsKey, { active: selected }); - - const connections = connectionsLoader.data as Connection[]; - - const grantedConnections = getComputed(() => connections.filter(connection => state.state.grantedSubjects.includes(connection.id))); - - useAutoLoad(GrantedConnections, state, selected && !loaded); - - if (!selected) { - return null; - } - - let info: TLocalizationToken | null = null; - - const cloudExists = connections.some(isCloudConnection); - - if (cloudExists) { - info = 'cloud_connections_access_placeholder'; - } - - if (formState.mode === 'edit' && state.changed) { - info = 'ui_save_reminder'; - } - - return styled(style)( - - {() => - styled(style)( - - {!connections.length ? ( - - {translate('administration_teams_team_granted_connections_empty')} - - ) : ( - <> - {info && } - - - {state.state.editing && ( - - )} - - - )} - , - ) - } - , - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsList.tsx deleted file mode 100644 index 4ded2298ce..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsList.tsx +++ /dev/null @@ -1,137 +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 { observable } from 'mobx'; -import { observer } from 'mobx-react-lite'; -import { useCallback, useState } from 'react'; -import styled, { css } from 'reshadow'; - -import { - Button, - getComputed, - getSelectedItems, - Group, - Table, - TableBody, - TableColumnValue, - TableItem, - useObjectRef, - useStyles, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import { Connection, DBDriverResource } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import type { TLocalizationToken } from '@cloudbeaver/core-localization'; - -import { getFilteredConnections } from './getFilteredConnections'; -import { GrantedConnectionsTableHeader, IFilterState } from './GrantedConnectionsTableHeader/GrantedConnectionsTableHeader'; -import { GrantedConnectionsTableInnerHeader } from './GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader'; -import { GrantedConnectionsTableItem } from './GrantedConnectionsTableItem'; - -const styles = css` - Group { - position: relative; - } - Group, - container, - table-container { - height: 100%; - } - container { - display: flex; - flex-direction: column; - width: 100%; - } - GrantedConnectionsTableHeader { - flex: 0 0 auto; - } - table-container { - overflow: auto; - } - Table { - composes: theme-background-surface theme-text-on-surface from global; - width: 100%; - } -`; - -interface Props { - grantedConnections: Connection[]; - disabled: boolean; - onRevoke: (subjectIds: string[]) => void; - onEdit: () => void; -} - -export const GrantedConnectionList = observer(function GrantedConnectionList({ grantedConnections, disabled, onRevoke, onEdit }) { - const props = useObjectRef({ onRevoke, onEdit }); - const style = useStyles(styles); - const translate = useTranslate(); - - const driversResource = useService(DBDriverResource); - - const [selectedSubjects] = useState>(() => observable(new Map())); - const [filterState] = useState(() => observable({ filterValue: '' })); - - const connections = getFilteredConnections(grantedConnections, filterState.filterValue); - const keys = connections.map(connection => connection.id); - - const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); - - const revoke = useCallback(() => { - props.onRevoke(getSelectedItems(selectedSubjects)); - selectedSubjects.clear(); - }, []); - - let tableInfoText: TLocalizationToken | null = null; - if (!connections.length) { - if (filterState.filterValue) { - tableInfoText = 'ui_search_no_result_placeholder'; - } else { - tableInfoText = 'ui_no_items_placeholder'; - } - } - - return styled(style)( - - - - - - - - - - - {tableInfoText && ( - - {translate(tableInfoText)} - - )} - {connections.map(connection => { - const driver = driversResource.get(connection.driverId); - return ( - - ); - })} - -
-
-
-
, - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTabService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTabService.ts deleted file mode 100644 index 209a3ae815..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTabService.ts +++ /dev/null @@ -1,110 +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 React from 'react'; - -import { TeamsResource } from '@cloudbeaver/core-authentication'; -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { executorHandlerFilter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { GraphQLService } from '@cloudbeaver/core-sdk'; -import { isArraysEqual, MetadataValueGetter } from '@cloudbeaver/core-utils'; - -import { teamContext } from '../Contexts/teamContext'; -import type { ITeamFormProps, ITeamFormSubmitData } from '../ITeamFormProps'; -import { TeamFormService } from '../TeamFormService'; -import type { IGrantedConnectionsTabState } from './IGrantedConnectionsTabState'; - -const GrantedConnections = React.lazy(async () => { - const { GrantedConnections } = await import('./GrantedConnections'); - return { default: GrantedConnections }; -}); - -@injectable() -export class GrantedConnectionsTabService extends Bootstrap { - private readonly key: string; - - constructor( - private readonly teamFormService: TeamFormService, - private readonly teamsResource: TeamsResource, - private readonly graphQLService: GraphQLService, - private readonly notificationService: NotificationService, - private readonly projectInfoResource: ProjectInfoResource, - ) { - super(); - this.key = 'granted-connections'; - } - - register(): void { - this.teamFormService.tabsContainer.add({ - key: this.key, - name: 'administration_teams_team_granted_connections_tab_title', - title: 'administration_teams_team_granted_connections_tab_title', - order: 3, - stateGetter: context => this.stateGetter(context), - isHidden: () => !this.isEnabled(), - panel: () => GrantedConnections, - }); - - this.teamFormService.afterFormSubmittingTask.addHandler(executorHandlerFilter(() => this.isEnabled(), this.save.bind(this))); - - this.teamFormService.configureTask.addHandler(() => this.projectInfoResource.load(CachedMapAllKey)); - } - - load(): Promise | void {} - - private isEnabled(): boolean { - return this.projectInfoResource.values.some(isGlobalProject); - } - - private stateGetter(context: ITeamFormProps): MetadataValueGetter { - return () => ({ - loading: false, - loaded: false, - editing: false, - grantedSubjects: [], - initialGrantedSubjects: [], - }); - } - - private async save(data: ITeamFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(teamContext); - const status = contexts.getContext(this.teamFormService.configurationStatusContext); - - if (!status.saved) { - return; - } - - const state = this.teamFormService.tabsContainer.getTabState(data.state.partsState, this.key, { state: data.state }); - - if (!config.teamId || !state.loaded) { - return; - } - - const grantInfo = await this.teamsResource.getSubjectConnectionAccess(config.teamId); - const initial = grantInfo.map(info => info.connectionId); - - const changed = !isArraysEqual(initial, state.grantedSubjects); - - if (!changed) { - return; - } - - try { - await this.graphQLService.sdk.setSubjectConnectionAccess({ - subjectId: config.teamId, - connections: state.grantedSubjects, - }); - - state.loaded = false; - } catch (exception: any) { - this.notificationService.logException(exception); - } - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.tsx deleted file mode 100644 index e99c1e56f7..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.tsx +++ /dev/null @@ -1,56 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { Filter, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; - -export interface IFilterState { - filterValue: string; -} - -interface Props extends React.PropsWithChildren { - filterState: IFilterState; - disabled: boolean; - className?: string; -} - -const styles = css` - buttons { - display: flex; - gap: 16px; - } - header { - composes: theme-border-color-background theme-background-surface theme-text-on-surface from global; - overflow: hidden; - position: sticky; - top: 0; - z-index: 1; - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px; - gap: 16px; - border-bottom: 1px solid; - } -`; - -export const GrantedConnectionsTableHeader = observer(function GrantedConnectionsTableHeader({ filterState, disabled, className, children }) { - const translate = useTranslate(); - return styled(useStyles(styles))( -
- - {children} -
, - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/IGrantedConnectionsTabState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/IGrantedConnectionsTabState.ts deleted file mode 100644 index 54fbe4f88e..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/IGrantedConnectionsTabState.ts +++ /dev/null @@ -1,15 +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. - */ - -export interface IGrantedConnectionsTabState { - loading: boolean; - loaded: boolean; - grantedSubjects: string[]; - initialGrantedSubjects: string[]; - editing: boolean; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/getFilteredConnections.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/getFilteredConnections.ts deleted file mode 100644 index 2853036bb2..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/getFilteredConnections.ts +++ /dev/null @@ -1,19 +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 { isCloudConnection } from '@cloudbeaver/core-connections'; -import type { DatabaseConnectionFragment } from '@cloudbeaver/core-sdk'; - -/** - * @param {DatabaseConnectionFragment[]} connections - * @param {string} filter - */ -export function getFilteredConnections(connections: DatabaseConnectionFragment[], filter: string): DatabaseConnectionFragment[] { - return connections - .filter(connection => connection.name.toLowerCase().includes(filter.toLowerCase()) && !isCloudConnection(connection)) - .sort((a, b) => a.name.localeCompare(b.name)); -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/useGrantedConnections.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/useGrantedConnections.tsx deleted file mode 100644 index 66f3f2478e..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/useGrantedConnections.tsx +++ /dev/null @@ -1,83 +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 { action, computed, observable } from 'mobx'; - -import { TeamInfo, TeamsResource } from '@cloudbeaver/core-authentication'; -import { useObservableRef } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { useTabState } from '@cloudbeaver/core-ui'; -import { ILoadableState, isArraysEqual } from '@cloudbeaver/core-utils'; - -import type { TeamFormMode } from '../ITeamFormProps'; -import type { IGrantedConnectionsTabState } from './IGrantedConnectionsTabState'; - -interface State extends ILoadableState { - state: IGrantedConnectionsTabState; - changed: boolean; - edit: () => void; - revoke: (subjectIds: string[]) => void; - grant: (subjectIds: string[]) => void; - load: () => Promise; -} - -export function useGrantedConnections(team: TeamInfo, mode: TeamFormMode): Readonly { - const resource = useService(TeamsResource); - const notificationService = useService(NotificationService); - const state = useTabState(); - - return useObservableRef( - () => ({ - get changed() { - return !isArraysEqual(this.state.initialGrantedSubjects, this.state.grantedSubjects); - }, - isLoading() { - return this.state.loading; - }, - isLoaded() { - return this.state.loaded; - }, - isError() { - return false; - }, - edit() { - this.state.editing = !this.state.editing; - }, - grant(subjectIds: string[]) { - this.state.grantedSubjects.push(...subjectIds); - }, - revoke(subjectIds: string[]) { - this.state.grantedSubjects = this.state.grantedSubjects.filter(subject => !subjectIds.includes(subject)); - }, - async load() { - if (this.state.loaded || this.state.loading) { - return; - } - - try { - this.state.loading = true; - - if (this.mode === 'edit') { - const grantInfo = await this.resource.getSubjectConnectionAccess(this.team.teamId); - this.state.grantedSubjects = grantInfo.map(subject => subject.dataSourceId); - this.state.initialGrantedSubjects = this.state.grantedSubjects.slice(); - } - - this.state.loaded = true; - } catch (exception: any) { - this.notificationService.logException(exception, `Error getting granted connections for "${this.team.teamId}"`); - } finally { - this.state.loading = false; - } - }, - }), - { state: observable.ref, changed: computed, grant: action.bound, revoke: action.bound, edit: action.bound }, - { state, team, mode, resource, notificationService }, - ['load'], - ); -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUserList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUserList.tsx deleted file mode 100644 index b40ea57a2c..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUserList.tsx +++ /dev/null @@ -1,137 +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 { observable } from 'mobx'; -import { observer } from 'mobx-react-lite'; -import { useCallback, useState } from 'react'; -import styled, { css } from 'reshadow'; - -import { UsersResource } from '@cloudbeaver/core-authentication'; -import { - Button, - getComputed, - getSelectedItems, - Group, - Table, - TableBody, - TableColumnValue, - TableItem, - useObjectRef, - useStyles, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import type { TLocalizationToken } from '@cloudbeaver/core-localization'; -import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; - -import { getFilteredUsers } from './getFilteredUsers'; -import { GrantedUsersTableHeader, IFilterState } from './GrantedUsersTableHeader/GrantedUsersTableHeader'; -import { GrantedUsersTableInnerHeader } from './GrantedUsersTableHeader/GrantedUsersTableInnerHeader'; -import { GrantedUsersTableItem } from './GrantedUsersTableItem'; - -const styles = css` - Table { - composes: theme-background-surface theme-text-on-surface from global; - } - Group { - position: relative; - } - Group, - container, - table-container { - height: 100%; - } - container { - display: flex; - flex-direction: column; - width: 100%; - } - GrantedUsersTableHeader { - flex: 0 0 auto; - } - table-container { - overflow: auto; - } -`; - -interface Props { - grantedUsers: AdminUserInfoFragment[]; - disabled: boolean; - onRevoke: (subjectIds: string[]) => void; - onEdit: () => void; -} - -export const GrantedUserList = observer(function GrantedUserList({ grantedUsers, disabled, onRevoke, onEdit }) { - const props = useObjectRef({ onRevoke, onEdit }); - const style = useStyles(styles); - const translate = useTranslate(); - - const usersResource = useService(UsersResource); - - const [selectedSubjects] = useState>(() => observable(new Map())); - const [filterState] = useState(() => observable({ filterValue: '' })); - - const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); - - const users = getFilteredUsers(grantedUsers, filterState.filterValue); - const keys = users.map(user => user.userId); - - const revoke = useCallback(() => { - props.onRevoke(getSelectedItems(selectedSubjects)); - selectedSubjects.clear(); - }, []); - - let tableInfoText: TLocalizationToken | null = null; - if (!users.length) { - if (filterState.filterValue) { - tableInfoText = 'ui_search_no_result_placeholder'; - } else { - tableInfoText = 'ui_no_items_placeholder'; - } - } - - return styled(style)( - - - - - - - - !usersResource.isActiveUser(item)}> - - - {tableInfoText && ( - - {translate(tableInfoText)} - - )} - {users.map(user => { - const activeUser = usersResource.isActiveUser(user.userId); - return ( - - ); - })} - -
-
-
-
, - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx deleted file mode 100644 index b86a554097..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx +++ /dev/null @@ -1,100 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { AdminUser, UsersResource, UsersResourceFilterKey } from '@cloudbeaver/core-authentication'; -import { - ColoredContainer, - Container, - getComputed, - Group, - InfoItem, - Loader, - TextPlaceholder, - useAutoLoad, - useResource, - useStyles, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import { CachedResourceOffsetPageListKey } from '@cloudbeaver/core-resource'; -import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; - -import type { ITeamFormProps } from '../ITeamFormProps'; -import { GrantedUserList } from './GrantedUserList'; -import { useGrantedUsers } from './useGrantedUsers'; -import { UserList } from './UserList'; - -const styles = css` - ColoredContainer { - flex: 1; - height: 100%; - box-sizing: border-box; - } - Group { - max-height: 100%; - position: relative; - overflow: auto !important; - } - Loader { - z-index: 2; - } -`; - -export const GrantedUsers: TabContainerPanelComponent = observer(function GrantedUsers({ tabId, state: formState }) { - const style = useStyles(styles); - const translate = useTranslate(); - - const state = useGrantedUsers(formState.config, formState.mode); - const { selected } = useTab(tabId); - - const users = useResource(GrantedUsers, UsersResource, CachedResourceOffsetPageListKey(0, 1000).setTarget(UsersResourceFilterKey()), { - active: selected, - }); - - const grantedUsers = getComputed(() => - users.data.filter((user): user is AdminUser => !!user && state.state.grantedUsers.includes(user.userId)), - ); - - useAutoLoad(GrantedUsers, state, selected && !state.state.loaded); - - if (!selected) { - return null; - } - - return styled(style)( - - {() => - styled(style)( - - {!users.resource.values.length ? ( - - {translate('administration_teams_team_granted_users_empty')} - - ) : ( - <> - {formState.mode === 'edit' && state.changed && } - - - {state.state.editing && ( - - )} - - - )} - , - ) - } - , - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTabService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTabService.ts deleted file mode 100644 index d5fd23360b..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTabService.ts +++ /dev/null @@ -1,118 +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 React from 'react'; - -import { TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { isArraysEqual, MetadataValueGetter } from '@cloudbeaver/core-utils'; - -import { teamContext } from '../Contexts/teamContext'; -import type { ITeamFormProps, ITeamFormSubmitData } from '../ITeamFormProps'; -import { TeamFormService } from '../TeamFormService'; -import type { IGrantedUsersTabState } from './IGrantedUsersTabState'; - -const GrantedUsers = React.lazy(async () => { - const { GrantedUsers } = await import('./GrantedUsers'); - return { default: GrantedUsers }; -}); - -@injectable() -export class GrantedUsersTabService extends Bootstrap { - private readonly key: string; - - constructor( - private readonly teamFormService: TeamFormService, - private readonly usersResource: UsersResource, - private readonly teamsResource: TeamsResource, - private readonly notificationService: NotificationService, - ) { - super(); - this.key = 'granted-users'; - } - - register(): void { - this.teamFormService.tabsContainer.add({ - key: this.key, - name: 'administration_teams_team_granted_users_tab_title', - title: 'administration_teams_team_granted_users_tab_title', - order: 2, - stateGetter: context => this.stateGetter(context), - panel: () => GrantedUsers, - }); - - this.teamFormService.afterFormSubmittingTask.addHandler(this.save.bind(this)); - } - - load(): void {} - - private stateGetter(context: ITeamFormProps): MetadataValueGetter { - return () => ({ - loading: false, - loaded: false, - editing: false, - grantedUsers: [], - initialGrantedUsers: [], - }); - } - - private async save(data: ITeamFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(teamContext); - const status = contexts.getContext(this.teamFormService.configurationStatusContext); - - if (!status.saved) { - return; - } - - const state = this.teamFormService.tabsContainer.getTabState(data.state.partsState, this.key, { state: data.state }); - - if (!config.teamId || !state.loaded) { - return; - } - - const initial = await this.teamsResource.loadGrantedUsers(config.teamId); - - const changed = !isArraysEqual(initial, state.grantedUsers); - - if (!changed) { - return; - } - - const granted: string[] = []; - const revoked: string[] = []; - - const revokedUsers = initial.filter(user => !state.grantedUsers.includes(user)); - - try { - for (const user of revokedUsers) { - await this.usersResource.revokeTeam(user, config.teamId); - revoked.push(user); - } - - for (const user of state.grantedUsers) { - if (!initial.includes(user)) { - await this.usersResource.grantTeam(user, config.teamId); - granted.push(user); - } - } - - state.loaded = false; - } catch (exception: any) { - this.notificationService.logException(exception); - } - - if (granted.length) { - status.info(`Added users: "${granted.join(', ')}"`); - } - - if (revoked.length) { - status.info(`Deleted users: "${revoked.join(', ')}"`); - } - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.tsx deleted file mode 100644 index daa9300d5f..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.tsx +++ /dev/null @@ -1,56 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { Filter, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; - -export interface IFilterState { - filterValue: string; -} - -interface Props extends React.PropsWithChildren { - filterState: IFilterState; - disabled: boolean; - className?: string; -} - -const styles = css` - buttons { - display: flex; - gap: 16px; - } - header { - composes: theme-border-color-background theme-background-surface theme-text-on-surface from global; - overflow: hidden; - position: sticky; - top: 0; - z-index: 1; - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px; - gap: 16px; - border-bottom: 1px solid; - } -`; - -export const GrantedUsersTableHeader = observer(function GrantedUsersTableHeader({ filterState, disabled, className, children }) { - const translate = useTranslate(); - return styled(useStyles(styles))( -
- - {children} -
, - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx deleted file mode 100644 index ad33b5345e..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx +++ /dev/null @@ -1,29 +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 { observer } from 'mobx-react-lite'; - -import { TableColumnHeader, TableHeader, TableSelect, useTranslate } from '@cloudbeaver/core-blocks'; - -interface Props { - disabled?: boolean; - className?: string; -} - -export const GrantedUsersTableInnerHeader = observer(function GrantedUsersTableInnerHeader({ disabled, className }) { - const translate = useTranslate(); - - return ( - - - - - - {translate('administration_teams_team_granted_users_user_id')} - - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableItem.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableItem.tsx deleted file mode 100644 index abb9f21102..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsersTableItem.tsx +++ /dev/null @@ -1,42 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { StaticImage, TableColumnValue, TableItem, TableItemSelect } from '@cloudbeaver/core-blocks'; - -interface Props { - id: any; - name: string; - icon: string; - disabled: boolean; - iconTooltip?: string; - tooltip?: string; - className?: string; -} - -const style = css` - StaticImage { - display: flex; - width: 24px; - } -`; - -export const GrantedUsersTableItem = observer(function GrantedUsersTableItem({ id, name, icon, iconTooltip, tooltip, disabled, className }) { - return styled(style)( - - - - - - - - {name} - , - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/IGrantedUsersTabState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/IGrantedUsersTabState.ts deleted file mode 100644 index 5aae1fcc94..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/IGrantedUsersTabState.ts +++ /dev/null @@ -1,15 +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. - */ - -export interface IGrantedUsersTabState { - loading: boolean; - loaded: boolean; - grantedUsers: string[]; - initialGrantedUsers: string[]; - editing: boolean; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UserList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UserList.tsx deleted file mode 100644 index 08d734d992..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UserList.tsx +++ /dev/null @@ -1,128 +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 { observable } from 'mobx'; -import { observer } from 'mobx-react-lite'; -import { useCallback, useState } from 'react'; -import styled, { css } from 'reshadow'; - -import { UsersResource } from '@cloudbeaver/core-authentication'; -import { - Button, - getComputed, - getSelectedItems, - Group, - Table, - TableBody, - TableColumnValue, - TableItem, - useObjectRef, - useStyles, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; - -import { getFilteredUsers } from './getFilteredUsers'; -import { GrantedUsersTableHeader, IFilterState } from './GrantedUsersTableHeader/GrantedUsersTableHeader'; -import { GrantedUsersTableInnerHeader } from './GrantedUsersTableHeader/GrantedUsersTableInnerHeader'; -import { GrantedUsersTableItem } from './GrantedUsersTableItem'; - -const styles = css` - Table { - composes: theme-background-surface theme-text-on-surface from global; - } - Group { - position: relative; - } - Group, - container, - table-container { - height: 100%; - } - container { - display: flex; - flex-direction: column; - width: 100%; - } - table-container { - overflow: auto; - } - GrantedUsersTableHeader { - flex: 0 0 auto; - } -`; - -interface Props { - userList: AdminUserInfoFragment[]; - grantedUsers: string[]; - disabled: boolean; - onGrant: (subjectIds: string[]) => void; -} - -export const UserList = observer(function UserList({ userList, grantedUsers, disabled, onGrant }) { - const props = useObjectRef({ onGrant }); - const style = useStyles(styles); - const translate = useTranslate(); - - const usersResource = useService(UsersResource); - - const [selectedSubjects] = useState>(() => observable(new Map())); - const [filterState] = useState(() => observable({ filterValue: '' })); - - const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); - - const users = getFilteredUsers(userList, filterState.filterValue); - const keys = users.map(user => user.userId); - - const grant = useCallback(() => { - props.onGrant(getSelectedItems(selectedSubjects)); - selectedSubjects.clear(); - }, []); - - return styled(style)( - - - - - - - !(usersResource.isActiveUser(item) || grantedUsers.includes(item))} - > - - - {!users.length && filterState.filterValue && ( - - {translate('ui_search_no_result_placeholder')} - - )} - {users.map(user => { - const activeUser = usersResource.isActiveUser(user.userId); - return ( - - ); - })} - -
-
-
-
, - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/useGrantedUsers.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/useGrantedUsers.tsx deleted file mode 100644 index db1035983b..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/useGrantedUsers.tsx +++ /dev/null @@ -1,83 +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 { action, computed, observable } from 'mobx'; - -import { TeamInfo, TeamsResource } from '@cloudbeaver/core-authentication'; -import { useObservableRef } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { useTabState } from '@cloudbeaver/core-ui'; -import { ILoadableState, isArraysEqual } from '@cloudbeaver/core-utils'; - -import type { TeamFormMode } from '../ITeamFormProps'; -import type { IGrantedUsersTabState } from './IGrantedUsersTabState'; - -interface State extends ILoadableState { - state: IGrantedUsersTabState; - changed: boolean; - edit: () => void; - revoke: (subjectIds: string[]) => void; - grant: (subjectIds: string[]) => void; - load: () => Promise; -} - -export function useGrantedUsers(team: TeamInfo, mode: TeamFormMode): Readonly { - const resource = useService(TeamsResource); - const notificationService = useService(NotificationService); - const state = useTabState(); - - return useObservableRef( - () => ({ - get changed() { - return !isArraysEqual(this.state.initialGrantedUsers, this.state.grantedUsers); - }, - isLoading() { - return this.state.loading; - }, - isLoaded() { - return this.state.loaded; - }, - isError() { - return false; - }, - edit() { - this.state.editing = !this.state.editing; - }, - revoke(subjectIds: string[]) { - this.state.grantedUsers = this.state.grantedUsers.filter(subject => !subjectIds.includes(subject)); - }, - grant(subjectIds: string[]) { - this.state.grantedUsers.push(...subjectIds); - }, - async load() { - if (this.state.loaded || this.state.loading) { - return; - } - - try { - this.state.loading = true; - - if (this.mode === 'edit') { - const grantedUsers = await this.resource.loadGrantedUsers(this.team.teamId); - this.state.grantedUsers = grantedUsers; - this.state.initialGrantedUsers = this.state.grantedUsers.slice(); - } - - this.state.loaded = true; - } catch (exception: any) { - this.notificationService.logException(exception, "Can't load users info"); - } finally { - this.state.loading = false; - } - }, - }), - { state: observable.ref, changed: computed, edit: action.bound, revoke: action.bound, grant: action.bound }, - { state, team, mode, resource, notificationService }, - ['load'], - ); -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/ITeamFormProps.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/ITeamFormProps.ts deleted file mode 100644 index 5a4c3561b0..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/ITeamFormProps.ts +++ /dev/null @@ -1,46 +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 type { TeamInfo, TeamsResource } from '@cloudbeaver/core-authentication'; -import type { IExecutorHandlersCollection } from '@cloudbeaver/core-executor'; -import type { MetadataMap } from '@cloudbeaver/core-utils'; - -export type TeamFormMode = 'edit' | 'create'; - -export interface ITeamFormState { - mode: TeamFormMode; - config: TeamInfo; - partsState: MetadataMap; - - readonly info: TeamInfo | undefined; - readonly statusMessage: string | null; - readonly disabled: boolean; - readonly readonly: boolean; - readonly loading: boolean; - - readonly submittingTask: IExecutorHandlersCollection; - readonly resource: TeamsResource; - - readonly load: () => Promise; - readonly loadTeamInfo: () => Promise; - readonly save: () => Promise; - readonly setOptions: (mode: TeamFormMode) => this; -} - -export interface ITeamFormProps { - state: ITeamFormState; - onCancel?: () => void; -} - -export interface ITeamFormFillConfigData { - updated: boolean; - state: ITeamFormState; -} - -export interface ITeamFormSubmitData { - state: ITeamFormState; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamMetaParameters.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamMetaParameters.tsx deleted file mode 100644 index c0cd6e4f98..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamMetaParameters.tsx +++ /dev/null @@ -1,33 +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 { observer } from 'mobx-react-lite'; - -import { TeamMetaParametersResource } from '@cloudbeaver/core-authentication'; -import { Group, GroupTitle, ObjectPropertyInfoForm, useResource, useTranslate } from '@cloudbeaver/core-blocks'; - -import type { ITeamFormState } from '../ITeamFormProps'; - -interface IProps { - state: ITeamFormState; -} - -export const TeamMetaParameters = observer(function TeamMetaParameters({ state }) { - const teamMetaParameters = useResource(TeamMetaParameters, TeamMetaParametersResource, undefined); - const translate = useTranslate(); - - if (teamMetaParameters.data.length === 0) { - return null; - } - - return ( - - {translate('authentication_team_meta_parameters')} - - - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamOptions.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamOptions.tsx deleted file mode 100644 index 2455a60f2d..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamOptions.tsx +++ /dev/null @@ -1,53 +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 { observer } from 'mobx-react-lite'; -import { useRef } from 'react'; -import styled, { css } from 'reshadow'; - -import { ColoredContainer, Form, Group, InputField, Textarea, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; -import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; - -import type { ITeamFormProps } from '../ITeamFormProps'; -import { Permissions } from './Permissions'; -import { TeamMetaParameters } from './TeamMetaParameters'; - -const styles = css` - Form { - flex: 1; - overflow: auto; - } -`; - -export const TeamOptions: TabContainerPanelComponent = observer(function TeamOptions({ state }) { - const serverConfigResource = useResource(TeamOptions, ServerConfigResource, undefined); - const style = useStyles(styles); - const formRef = useRef(null); - const translate = useTranslate(); - const edit = state.mode === 'edit'; - - return styled(style)( -
- - - - {translate('administration_teams_team_id')} - - - {translate('administration_teams_team_name')} - - - - {!serverConfigResource.resource.distributed && } - - -
, - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamOptionsTabService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamOptionsTabService.ts deleted file mode 100644 index 74e39dcc45..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamOptionsTabService.ts +++ /dev/null @@ -1,150 +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 React from 'react'; - -import { TeamsResource } from '@cloudbeaver/core-authentication'; -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { LocalizationService } from '@cloudbeaver/core-localization'; -import { getUniqueName } from '@cloudbeaver/core-utils'; - -import { teamContext } from '../Contexts/teamContext'; -import type { ITeamFormFillConfigData, ITeamFormSubmitData } from '../ITeamFormProps'; -import { TeamFormService } from '../TeamFormService'; - -const TeamOptions = React.lazy(async () => { - const { TeamOptions } = await import('./TeamOptions'); - return { default: TeamOptions }; -}); - -@injectable() -export class TeamOptionsTabService extends Bootstrap { - constructor( - private readonly teamFormService: TeamFormService, - private readonly teamResource: TeamsResource, - private readonly localizationService: LocalizationService, - ) { - super(); - } - - register(): void { - this.teamFormService.tabsContainer.add({ - key: 'options', - name: 'ui_options', - order: 1, - panel: () => TeamOptions, - }); - - this.teamFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this)); - - this.teamFormService.formValidationTask.addHandler(this.validate.bind(this)); - - this.teamFormService.formSubmittingTask.addHandler(this.save.bind(this)); - - this.teamFormService.fillConfigTask.addHandler(this.fillConfig.bind(this)); - } - - load(): void {} - - private async prepareConfig({ state }: ITeamFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(teamContext); - - config.teamId = state.config.teamId; - - if (state.config.teamName) { - config.teamName = state.config.teamName.trim(); - - if (state.mode === 'create') { - const teamNames = this.teamResource.values.map(team => team.teamName).filter(Boolean) as string[]; - config.teamName = getUniqueName(config.teamName, teamNames); - } - } - - if (state.config.description) { - config.description = state.config.description; - } - - if (state.config.metaParameters) { - config.metaParameters = state.config.metaParameters; - } - - config.teamPermissions = [...state.config.teamPermissions]; - } - - private async validate({ state }: ITeamFormSubmitData, contexts: IExecutionContextProvider) { - const validation = contexts.getContext(this.teamFormService.configurationValidationContext); - - if (state.mode === 'create') { - if (!state.config.teamId.trim()) { - validation.error('administration_teams_team_info_id_invalid'); - } - - if (this.teamResource.has(state.config.teamId)) { - validation.error( - this.localizationService.translate('administration_teams_team_info_exists', undefined, { - teamId: state.config.teamId, - }), - ); - } - } - } - - private async save({ state }: ITeamFormSubmitData, contexts: IExecutionContextProvider) { - const status = contexts.getContext(this.teamFormService.configurationStatusContext); - const config = contexts.getContext(teamContext); - - const create = state.mode === 'create'; - - try { - if (create) { - const team = await this.teamResource.createTeam(config); - status.info('administration_teams_team_info_created'); - status.info(team.teamId); - } else { - const team = await this.teamResource.updateTeam(config); - - status.info('administration_teams_team_info_updated'); - status.info(team.teamId); - } - } catch (exception: any) { - if (create) { - status.error(exception, 'administration_teams_team_create_error'); - } else { - status.error(exception, 'administration_teams_team_save_error'); - } - } - } - - private fillConfig({ state, updated }: ITeamFormFillConfigData, contexts: IExecutionContextProvider) { - if (!updated) { - return; - } - - if (!state.info) { - return; - } - - if (state.info.teamId) { - state.config.teamId = state.info.teamId; - } - - if (state.info.teamName) { - state.config.teamName = state.info.teamName; - } - - if (state.info.description) { - state.config.description = state.info.description; - } - - if (state.info.metaParameters) { - state.config.metaParameters = state.info.metaParameters; - } - - state.config.teamPermissions = [...state.info.teamPermissions]; - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamForm.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamForm.tsx deleted file mode 100644 index da1a8606a6..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamForm.tsx +++ /dev/null @@ -1,150 +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 { observer } from 'mobx-react-lite'; -import { useEffect } from 'react'; -import styled, { css } from 'reshadow'; - -import type { TeamInfo } from '@cloudbeaver/core-authentication'; -import { IconOrImage, Loader, Placeholder, useExecutor, useObjectRef, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { BASE_TAB_STYLES, TabList, TabPanelList, TabsState, UNDERLINE_TAB_BIG_STYLES, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui'; - -import { teamContext } from './Contexts/teamContext'; -import type { ITeamFormState } from './ITeamFormProps'; -import { TeamFormService } from './TeamFormService'; - -const tabsStyles = css` - TabList { - position: relative; - flex-shrink: 0; - align-items: center; - } -`; - -const topBarStyles = css` - team-top-bar { - composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global; - position: relative; - display: flex; - padding-top: 16px; - - &:before { - content: ''; - position: absolute; - bottom: 0; - width: 100%; - border-bottom: solid 2px; - border-color: inherit; - } - } - team-top-bar-tabs { - flex: 1; - } - - team-top-bar-actions { - display: flex; - align-items: center; - padding: 0 24px; - gap: 16px; - } - - team-status-message { - composes: theme-typography--caption from global; - height: 24px; - padding: 0 16px; - display: flex; - align-items: center; - gap: 8px; - - & IconOrImage { - height: 24px; - width: 24px; - } - } -`; - -const formStyles = css` - box { - composes: theme-background-secondary theme-text-on-secondary from global; - display: flex; - flex-direction: column; - flex: 1; - height: 100%; - overflow: auto; - } - content-box { - composes: theme-background-secondary theme-border-color-background from global; - position: relative; - display: flex; - flex: 1; - flex-direction: column; - overflow: auto; - } -`; - -interface Props { - state: ITeamFormState; - onCancel?: () => void; - onSave?: (team: TeamInfo) => void; - className?: string; -} - -export const TeamForm = observer(function TeamForm({ state, onCancel, onSave = () => {}, className }) { - const translate = useTranslate(); - const props = useObjectRef({ onSave }); - const style = [BASE_TAB_STYLES, tabsStyles, UNDERLINE_TAB_STYLES, UNDERLINE_TAB_BIG_STYLES]; - const styles = useStyles(style, topBarStyles, formStyles); - const service = useService(TeamFormService); - - useExecutor({ - executor: state.submittingTask, - postHandlers: [ - function save(data, contexts) { - const validation = contexts.getContext(service.configurationValidationContext); - const state = contexts.getContext(service.configurationStatusContext); - const config = contexts.getContext(teamContext); - - if (validation.valid && state.saved) { - props.onSave(config); - } - }, - ], - }); - - useEffect(() => { - state.loadTeamInfo(); - }, []); - - return styled(styles)( - - - - - - {state.statusMessage && ( - <> - - {translate(state.statusMessage)} - - )} - - - - - - - - - - - - - - , - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamFormBaseActions.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamFormBaseActions.tsx deleted file mode 100644 index b57d5dc92b..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamFormBaseActions.tsx +++ /dev/null @@ -1,29 +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 { observer } from 'mobx-react-lite'; - -import { Button, PlaceholderComponent, useTranslate } from '@cloudbeaver/core-blocks'; - -import type { ITeamFormProps } from './ITeamFormProps'; - -export const TeamFormBaseActions: PlaceholderComponent = observer(function TeamFormBaseActions({ state, onCancel }) { - const translate = useTranslate(); - - return ( - <> - {onCancel && ( - - )} - - - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamFormService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamFormService.ts deleted file mode 100644 index 8e70c000cc..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamFormService.ts +++ /dev/null @@ -1,142 +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 React from 'react'; - -import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; -import { injectable } from '@cloudbeaver/core-di'; -import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; -import { ExecutorHandlersCollection, ExecutorInterrupter, IExecutorHandler, IExecutorHandlersCollection } from '@cloudbeaver/core-executor'; -import { TabsContainer } from '@cloudbeaver/core-ui'; - -import type { ITeamFormFillConfigData, ITeamFormProps, ITeamFormState, ITeamFormSubmitData } from './ITeamFormProps'; - -const TeamFormBaseActions = React.lazy(async () => { - const { TeamFormBaseActions } = await import('./TeamFormBaseActions'); - return { default: TeamFormBaseActions }; -}); - -export interface ITeamFormValidation { - valid: boolean; - messages: string[]; - info: (message: string) => void; - error: (message: string) => void; -} - -export interface ITeamFormStatus { - saved: boolean; - messages: string[]; - exception: Error | null; - info: (message: string) => void; - error: (exception: Error | null, message?: string) => void; -} - -@injectable() -export class TeamFormService { - readonly tabsContainer: TabsContainer; - readonly actionsContainer: PlaceholderContainer; - - readonly configureTask: IExecutorHandlersCollection; - readonly fillConfigTask: IExecutorHandlersCollection; - readonly prepareConfigTask: IExecutorHandlersCollection; - readonly formValidationTask: IExecutorHandlersCollection; - readonly formSubmittingTask: IExecutorHandlersCollection; - readonly afterFormSubmittingTask: IExecutorHandlersCollection; - readonly formStateTask: IExecutorHandlersCollection; - - constructor(private readonly notificationService: NotificationService) { - this.tabsContainer = new TabsContainer('Team settings'); - this.actionsContainer = new PlaceholderContainer(); - this.configureTask = new ExecutorHandlersCollection(); - this.fillConfigTask = new ExecutorHandlersCollection(); - this.prepareConfigTask = new ExecutorHandlersCollection(); - this.formSubmittingTask = new ExecutorHandlersCollection(); - this.afterFormSubmittingTask = new ExecutorHandlersCollection(); - this.formValidationTask = new ExecutorHandlersCollection(); - this.formStateTask = new ExecutorHandlersCollection(); - - this.formSubmittingTask.before(this.formValidationTask).before(this.prepareConfigTask).next(this.afterFormSubmittingTask); - - this.formStateTask.before(this.prepareConfigTask, state => ({ state, submitType: 'submit' })); - - this.formSubmittingTask.addPostHandler(this.showSubmittingStatusMessage); - this.formValidationTask.addPostHandler(this.ensureValidation); - - this.actionsContainer.add(TeamFormBaseActions); - } - - configurationValidationContext = (): ITeamFormValidation => ({ - valid: true, - messages: [], - info(message: string) { - this.messages.push(message); - }, - error(message: string) { - this.messages.push(message); - this.valid = false; - }, - }); - - configurationStatusContext = (): ITeamFormStatus => ({ - saved: true, - messages: [], - exception: null, - info(message: string) { - this.messages.push(message); - }, - error(exception: Error | null, message?: string) { - if (message) { - this.messages.push(message); - } - this.saved = false; - this.exception = exception; - }, - }); - - private readonly showSubmittingStatusMessage: IExecutorHandler = (data, contexts) => { - const status = contexts.getContext(this.configurationStatusContext); - - if (!status.saved) { - ExecutorInterrupter.interrupt(contexts); - } - - if (status.messages.length > 0) { - if (status.exception) { - this.notificationService.logException(status.exception, status.messages[0], status.messages.slice(1).join('\n')); - } else { - this.notificationService.notify( - { - title: status.messages[0], - message: status.messages.slice(1).join('\n'), - }, - status.saved ? ENotificationType.Success : ENotificationType.Error, - ); - } - } - }; - - private readonly ensureValidation: IExecutorHandler = (data, contexts) => { - const validation = contexts.getContext(this.configurationValidationContext); - - if (!validation.valid) { - ExecutorInterrupter.interrupt(contexts); - } - - if (validation.messages.length > 0) { - this.notificationService.notify( - { - title: - data.state.mode === 'edit' - ? 'administration_identity_providers_provider_save_error' - : 'administration_identity_providers_provider_create_error', - message: validation.messages.join('\n'), - }, - validation.valid ? ENotificationType.Info : ENotificationType.Error, - ); - } - }; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamFormState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamFormState.ts deleted file mode 100644 index bc971028fe..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamFormState.ts +++ /dev/null @@ -1,147 +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 { computed, makeObservable, observable } from 'mobx'; - -import type { TeamInfo, TeamsResource } from '@cloudbeaver/core-authentication'; -import { Executor, IExecutionContextProvider, IExecutor } from '@cloudbeaver/core-executor'; -import { MetadataMap } from '@cloudbeaver/core-utils'; - -import { teamFormConfigureContext } from './Contexts/teamFormConfigureContext'; -import { ITeamFormStateInfo, teamFormStateContext } from './Contexts/teamFormStateContext'; -import type { ITeamFormState, ITeamFormSubmitData, TeamFormMode } from './ITeamFormProps'; -import type { TeamFormService } from './TeamFormService'; - -export class TeamFormState implements ITeamFormState { - mode: TeamFormMode; - config: TeamInfo; - statusMessage: string | null; - configured: boolean; - partsState: MetadataMap; - - get info(): TeamInfo | undefined { - if (!this.config.teamId) { - return undefined; - } - return this.resource.get(this.config.teamId); - } - - get loading(): boolean { - return this.loadTeamTask.executing || this.submittingTask.executing; - } - - get disabled(): boolean { - return this.loading || !!this.stateInfo?.disabled || this.loadTeamTask.executing; - } - - get readonly(): boolean { - return false; - } - - readonly resource: TeamsResource; - - readonly service: TeamFormService; - readonly submittingTask: IExecutor; - - private stateInfo: ITeamFormStateInfo | null; - private readonly loadTeamTask: IExecutor; - private readonly formStateTask: IExecutor; - - constructor(service: TeamFormService, resource: TeamsResource) { - this.resource = resource; - this.config = { - teamId: '', - teamPermissions: [], - metaParameters: {}, - }; - - this.stateInfo = null; - this.service = service; - this.formStateTask = new Executor(this, () => true); - this.loadTeamTask = new Executor(this, () => true); - this.submittingTask = new Executor(); - this.statusMessage = null; - this.configured = false; - this.partsState = new MetadataMap(); - this.mode = 'create'; - - makeObservable(this, { - mode: observable, - config: observable, - statusMessage: observable, - info: computed, - readonly: computed, - }); - - this.save = this.save.bind(this); - this.loadInfo = this.loadInfo.bind(this); - this.updateFormState = this.updateFormState.bind(this); - - this.formStateTask.addCollection(service.formStateTask).addPostHandler(this.updateFormState); - - this.loadTeamTask - .before(service.configureTask) - .addPostHandler(this.loadInfo) - .next(service.fillConfigTask, (state, contexts) => { - const configuration = contexts.getContext(teamFormConfigureContext); - - return { - state, - updated: state.info !== configuration.info || !this.configured, - }; - }) - .next(this.formStateTask); - } - - async load(): Promise {} - - async loadTeamInfo(): Promise { - await this.loadTeamTask.execute(this); - - return this.info; - } - - setOptions(mode: TeamFormMode): this { - this.mode = mode; - return this; - } - - setConfig(config: TeamInfo): this { - this.config = config; - return this; - } - - async save(): Promise { - await this.submittingTask.executeScope( - { - state: this, - }, - this.service.formSubmittingTask, - ); - } - - private updateFormState(data: ITeamFormState, contexts: IExecutionContextProvider): void { - const context = contexts.getContext(teamFormStateContext); - - this.statusMessage = context.statusMessage; - - this.stateInfo = context; - this.configured = true; - } - - private async loadInfo(data: ITeamFormState, contexts: IExecutionContextProvider) { - if (!data.config.teamId) { - return; - } - - if (!this.resource.has(data.config.teamId)) { - return; - } - - await this.resource.load(data.config.teamId, ['includeMetaParameters']); - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsAdministrationNavService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsAdministrationNavService.ts index 67b2dc463c..78810dc3ee 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsAdministrationNavService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsAdministrationNavService.ts @@ -1,15 +1,15 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; -import { EUsersAdministrationSub, UsersAdministrationNavigationService } from '../UsersAdministrationNavigationService'; +import { EUsersAdministrationSub, UsersAdministrationNavigationService } from '../UsersAdministrationNavigationService.js'; -@injectable() +@injectable(() => [UsersAdministrationNavigationService]) export class TeamsAdministrationNavService { constructor(private readonly usersAdministrationNavigationService: UsersAdministrationNavigationService) {} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsAdministrationService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsAdministrationService.ts index cfedab4fe4..6eb62357c7 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsAdministrationService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsAdministrationService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/ConnectionList.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/ConnectionList.module.css new file mode 100644 index 0000000000..b808a2abc2 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/ConnectionList.module.css @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.group { + height: 100%; + + & .header { + flex: 0 0 auto; + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/ConnectionList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/ConnectionList.tsx new file mode 100644 index 0000000000..f686b688bd --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/ConnectionList.tsx @@ -0,0 +1,101 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useState } from 'react'; + +import { + Button, + Container, + getComputed, + getSelectedItems, + Group, + s, + Table, + TableBody, + TableColumnValue, + TableItem, + useObjectRef, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { type ConnectionInfoCustomOptions, type ConnectionInfoOrigin, DBDriverResource } from '@cloudbeaver/core-connections'; +import { useService } from '@cloudbeaver/core-di'; + +import styles from './ConnectionList.module.css'; +import { getFilteredConnections } from './getFilteredConnections.js'; +import { GrantedConnectionsTableHeader, type IFilterState } from './GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.js'; +import { GrantedConnectionsTableInnerHeader } from './GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader.js'; +import { GrantedConnectionsTableItem } from './GrantedConnectionsTableItem.js'; + +interface Props { + connectionList: ConnectionInfoCustomOptions[]; + connectionsOrigins: ConnectionInfoOrigin[]; + grantedSubjects: string[]; + disabled: boolean; + onGrant: (subjectIds: string[]) => void; +} + +export const ConnectionList = observer(function ConnectionList({ connectionList, connectionsOrigins, grantedSubjects, disabled, onGrant }) { + const props = useObjectRef({ onGrant }); + const style = useS(styles); + const translate = useTranslate(); + + const driversResource = useService(DBDriverResource); + + const [selectedSubjects] = useState>(() => observable(new Map())); + const [filterState] = useState(() => observable({ filterValue: '' })); + + const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); + + const grant = useCallback(() => { + props.onGrant(getSelectedItems(selectedSubjects)); + selectedSubjects.clear(); + }, []); + + const connections = getFilteredConnections(connectionList, connectionsOrigins, filterState.filterValue); + const keys = connectionList.map(connection => connection.id); + + return ( + + + + + + + + !grantedSubjects.includes(item)}> + + + {!connections.length && filterState.filterValue && ( + + {translate('ui_search_no_result_placeholder')} + + )} + {connections.map(connection => { + const driver = driversResource.get(connection.driverId); + return ( + + ); + })} + +
+
+
+ ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnections.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnections.module.css new file mode 100644 index 0000000000..293ac777b5 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnections.module.css @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.box { + flex: 1; + height: 100%; + box-sizing: border-box; +} + +.placeholderBox { + max-height: 100%; + position: relative; + overflow: auto !important; +} + +.loader { + z-index: 2; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnections.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnections.tsx new file mode 100644 index 0000000000..e8c4a5bfbb --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnections.tsx @@ -0,0 +1,125 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { useState } from 'react'; + +import { + Container, + getComputed, + Group, + InfoItem, + Loader, + s, + TextPlaceholder, + useAutoLoad, + useResource, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { + type ConnectionInfoCustomOptions, + ConnectionInfoCustomOptionsResource, + type ConnectionInfoOrigin, + ConnectionInfoOriginResource, + ConnectionInfoProjectKey, + DBDriverResource, + isCloudConnection, +} from '@cloudbeaver/core-connections'; +import type { TLocalizationToken } from '@cloudbeaver/core-localization'; +import { isGlobalProject, type ProjectInfo, ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; + +import type { TeamFormProps } from '../TeamsAdministrationFormService.js'; +import { ConnectionList } from './ConnectionList.js'; +import style from './GrantedConnections.module.css'; +import type { GrantedConnectionsFormPart } from './GrantedConnectionsFormPart.js'; +import { GrantedConnectionList } from './GrantedConnectionsList.js'; + +export const GrantedConnections: TabContainerPanelComponent = observer(function GrantedConnections({ tabId, formState }) { + const styles = useS(style); + const translate = useTranslate(); + + const tabState = useTabState(); + const { selected } = useTab(tabId); + const loaded = tabState.isLoaded(); + const [edit, setEdit] = useState(false); + const projects = useResource(GrantedConnections, ProjectInfoResource, CachedMapAllKey); + + const globalConnectionsKey = ConnectionInfoProjectKey( + ...(projects.data as Array).filter(isGlobalProject).map(project => project.id), + ); + + useResource(GrantedConnections, DBDriverResource, CachedMapAllKey, { active: selected }); + + const connectionsLoader = useResource(GrantedConnections, ConnectionInfoCustomOptionsResource, globalConnectionsKey, { active: selected }); + const connectionsOriginLoader = useResource(GrantedConnections, ConnectionInfoOriginResource, globalConnectionsKey, { active: selected }); + + const connections = connectionsLoader.data as ConnectionInfoCustomOptions[]; + + const grantedConnections = getComputed(() => connections.filter(connection => tabState.state.grantedSubjects.includes(connection.id))); + const connectionsOrigins = (connectionsOriginLoader.data ?? []) as ConnectionInfoOrigin[]; + + function toggleEdit() { + setEdit(value => !value); + } + + useAutoLoad(GrantedConnections, tabState, selected && !loaded); + + if (!selected) { + return null; + } + + let info: TLocalizationToken | null = null; + + const cloudExists = connectionsOrigins.some(connectionOrigin => isCloudConnection(connectionOrigin.origin)); + + if (cloudExists) { + info = 'cloud_connections_access_placeholder'; + } + + if (formState.mode === 'edit' && tabState.isChanged && !formState.isDisabled) { + info = 'ui_save_reminder'; + } + + return ( + + {() => ( + + {!connections.length ? ( + + {translate('administration_teams_team_granted_connections_empty')} + + ) : ( + <> + {info && } + + + {edit && ( + + )} + + + )} + + )} + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsFormPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsFormPart.ts new file mode 100644 index 0000000000..0a8821be29 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsFormPart.ts @@ -0,0 +1,94 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { TeamsResource } from '@cloudbeaver/core-authentication'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; +import { FormPart, type IFormState } from '@cloudbeaver/core-ui'; + +import type { ITeamFormState } from '../TeamsAdministrationFormService.js'; +import type { IGrantedConnectionsState } from './IGrantedConnectionsState.js'; + +function getInitialState(): IGrantedConnectionsState { + return { + grantedSubjects: [], + }; +} + +export class GrantedConnectionsFormPart extends FormPart { + constructor( + formState: IFormState, + private readonly projectInfoResource: ProjectInfoResource, + private readonly teamsResource: TeamsResource, + private readonly graphQLService: GraphQLService, + ) { + super(formState, getInitialState()); + + this.grant = this.grant.bind(this); + this.revoke = this.revoke.bind(this); + } + + protected override async loader(): Promise { + if (this.formState.mode === 'edit' && this.formState.state.teamId) { + const grantInfo = await this.teamsResource.getSubjectConnectionAccess(this.formState.state.teamId); + + this.setInitialState({ + ...getInitialState(), + grantedSubjects: grantInfo.map(info => info.connectionId), + }); + return; + } + + this.setInitialState(getInitialState()); + } + + protected override async saveChanges( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise { + const teamId = this.formState.state.teamId!; + const globalProject = this.projectInfoResource.values.find(isGlobalProject); + + if (!globalProject) { + throw new Error('The global project does not exist'); + } + + const { connectionsToRevoke, connectionsToGrant } = this.getConnectionsUpdates(this.initialState.grantedSubjects, this.state.grantedSubjects); + + if (connectionsToRevoke.length > 0) { + await this.graphQLService.sdk.deleteConnectionsAccess({ + projectId: globalProject.id, + subjects: [teamId], + connectionIds: connectionsToRevoke, + }); + } + + if (connectionsToGrant.length > 0) { + await this.graphQLService.sdk.addConnectionsAccess({ + projectId: globalProject.id, + subjects: [teamId], + connectionIds: connectionsToGrant, + }); + } + } + + grant(subjectIds: string[]) { + this.state.grantedSubjects.push(...subjectIds); + } + + revoke(subjectIds: string[]) { + this.state.grantedSubjects = this.state.grantedSubjects.filter(subject => !subjectIds.includes(subject)); + } + + private getConnectionsUpdates(current: string[], next: string[]): { connectionsToRevoke: string[]; connectionsToGrant: string[] } { + const connectionsToRevoke = current.filter(connectionId => !next.includes(connectionId)); + const connectionsToGrant = next.filter(connectionId => !current.includes(connectionId)); + + return { connectionsToRevoke, connectionsToGrant }; + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsList.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsList.module.css new file mode 100644 index 0000000000..b808a2abc2 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsList.module.css @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.group { + height: 100%; + + & .header { + flex: 0 0 auto; + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsList.tsx new file mode 100644 index 0000000000..75e543d6b0 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsList.tsx @@ -0,0 +1,122 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useState } from 'react'; + +import { + Button, + Container, + getComputed, + getSelectedItems, + Group, + s, + Table, + TableBody, + TableColumnValue, + TableItem, + useObjectRef, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { type ConnectionInfoCustomOptions, type ConnectionInfoOrigin, DBDriverResource } from '@cloudbeaver/core-connections'; +import { useService } from '@cloudbeaver/core-di'; +import type { TLocalizationToken } from '@cloudbeaver/core-localization'; + +import { getFilteredConnections } from './getFilteredConnections.js'; +import style from './GrantedConnectionsList.module.css'; +import { GrantedConnectionsTableHeader, type IFilterState } from './GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.js'; +import { GrantedConnectionsTableInnerHeader } from './GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader.js'; +import { GrantedConnectionsTableItem } from './GrantedConnectionsTableItem.js'; + +interface Props { + grantedConnections: ConnectionInfoCustomOptions[]; + connectionsOrigins: ConnectionInfoOrigin[]; + disabled: boolean; + onRevoke: (subjectIds: string[]) => void; + onEdit: () => void; +} + +export const GrantedConnectionList = observer(function GrantedConnectionList({ + connectionsOrigins, + grantedConnections, + disabled, + onRevoke, + onEdit, +}) { + const props = useObjectRef({ onRevoke, onEdit }); + const styles = useS(style); + const translate = useTranslate(); + + const driversResource = useService(DBDriverResource); + + const [selectedSubjects] = useState>(() => observable(new Map())); + const [filterState] = useState(() => observable({ filterValue: '' })); + + const connections = getFilteredConnections(grantedConnections, connectionsOrigins, filterState.filterValue); + const keys = grantedConnections.map(connection => connection.id); + + const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); + + const revoke = useCallback(() => { + props.onRevoke(getSelectedItems(selectedSubjects)); + selectedSubjects.clear(); + }, []); + + let tableInfoText: TLocalizationToken | null = null; + if (!connections.length) { + if (filterState.filterValue) { + tableInfoText = 'ui_search_no_result_placeholder'; + } else { + tableInfoText = 'ui_no_items_placeholder'; + } + } + + return ( + + + + + + + + + + + + + + {tableInfoText && ( + + {translate(tableInfoText)} + + )} + {connections.map(connection => { + const driver = driversResource.get(connection.driverId); + return ( + + ); + })} + +
+
+
+ ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTabService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTabService.ts new file mode 100644 index 0000000000..fda7756a1e --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTabService.ts @@ -0,0 +1,46 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; + +import { TeamsAdministrationFormService } from '../TeamsAdministrationFormService.js'; +import { getGrantedConnectionsFormPart } from './getGrantedConnectionsFormPart.js'; + +const GrantedConnections = importLazyComponent(() => import('./GrantedConnections.js').then(module => module.GrantedConnections)); + +@injectable(() => [ProjectInfoResource, TeamsAdministrationFormService]) +export class GrantedConnectionsTabService extends Bootstrap { + private readonly key: string; + + constructor( + private readonly projectInfoResource: ProjectInfoResource, + private readonly teamsAdministrationFormService: TeamsAdministrationFormService, + ) { + super(); + this.key = 'granted-connections'; + } + + override register(): void { + this.teamsAdministrationFormService.parts.add({ + key: this.key, + name: 'administration_teams_team_granted_connections_tab_title', + title: 'administration_teams_team_granted_connections_tab_title', + order: 3, + panel: () => GrantedConnections, + isHidden: () => !this.isEnabled(), + stateGetter: props => () => getGrantedConnectionsFormPart(props.formState), + getLoader: () => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), + }); + } + + private isEnabled(): boolean { + return this.projectInfoResource.values.some(isGlobalProject); + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.module.css new file mode 100644 index 0000000000..d1ca9b666b --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.header { + composes: theme-border-color-background from global; + border-bottom: 1px solid; + flex-shrink: 0; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.tsx new file mode 100644 index 0000000000..1dda5fa1f5 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableHeader.tsx @@ -0,0 +1,39 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Container, Filter, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; + +import style from './GrantedConnectionsTableHeader.module.css'; + +export interface IFilterState { + filterValue: string; +} + +interface Props extends React.PropsWithChildren { + filterState: IFilterState; + disabled: boolean; + className?: string; +} + +export const GrantedConnectionsTableHeader = observer(function GrantedConnectionsTableHeader({ filterState, disabled, className, children }) { + const styles = useS(style); + const translate = useTranslate(); + + return ( + + + {children} + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader.tsx similarity index 95% rename from webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader.tsx rename to webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader.tsx index 0767ff42d0..157c2fd079 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableItem.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableItem.module.css new file mode 100644 index 0000000000..139e1f975d --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableItem.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.staticImage { + display: flex; + width: 24px; + min-width: 24px; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTableItem.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableItem.tsx similarity index 80% rename from webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTableItem.tsx rename to webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableItem.tsx index efb35abf6d..231da110c2 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnectionsTableItem.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTableItem.tsx @@ -1,15 +1,16 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; import { StaticImage, TableColumnValue, TableItem, TableItemSelect } from '@cloudbeaver/core-blocks'; +import style from './GrantedConnectionsTableItem.module.css'; + interface Props { id: any; name: string; @@ -21,13 +22,6 @@ interface Props { className?: string; } -const style = css` - StaticImage { - display: flex; - width: 24px; - } -`; - export const GrantedConnectionsTableItem = observer(function GrantedConnectionsTableItem({ id, name, @@ -38,18 +32,18 @@ export const GrantedConnectionsTableItem = observer(function GrantedConne disabled, className, }) { - return styled(style)( + return ( - {icon && } + {icon && } {name} {host && host} - , + ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/IGrantedConnectionsState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/IGrantedConnectionsState.ts new file mode 100644 index 0000000000..af5ca78536 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/IGrantedConnectionsState.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { schema } from '@cloudbeaver/core-utils'; + +export const TEAM_GRANTED_CONNECTIONS_SCHEMA = schema.object({ + grantedSubjects: schema.array(schema.string()), +}); + +export type IGrantedConnectionsState = schema.infer; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/getFilteredConnections.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/getFilteredConnections.ts new file mode 100644 index 0000000000..1eb3627f77 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/getFilteredConnections.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 ConnectionInfoCustomOptions, type ConnectionInfoOrigin, isCloudConnection } from '@cloudbeaver/core-connections'; + +/** + * @param {DatabaseConnectionFragment[]} connections + * @param {string} filter + */ +export function getFilteredConnections( + connections: ConnectionInfoCustomOptions[], + connectionsOrigin: ConnectionInfoOrigin[], + filter: string, +): ConnectionInfoCustomOptions[] { + const connectionsOriginsMap = new Map(); + + for (const connectionOrigin of connectionsOrigin) { + connectionsOriginsMap.set(connectionOrigin.id, connectionOrigin); + } + + return connections + .filter(connection => { + const originDetails = connectionsOriginsMap.get(connection.id); + + return connection.name.toLowerCase().includes(filter.toLowerCase()) && originDetails && !isCloudConnection(originDetails.origin); + }) + .sort((a, b) => a.name.localeCompare(b.name)); +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/getGrantedConnectionsFormPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/getGrantedConnectionsFormPart.ts new file mode 100644 index 0000000000..aef0647334 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedConnections/getGrantedConnectionsFormPart.ts @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { TeamsResource } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; +import type { IFormState } from '@cloudbeaver/core-ui'; + +import type { ITeamFormState } from '../TeamsAdministrationFormService.js'; +import { GrantedConnectionsFormPart } from './GrantedConnectionsFormPart.js'; + +const DATA_CONTEXT_TEAM_FORM_CONNECTION_ACCESS_PART = createDataContext('Team Form Connection Access Part'); + +export function getGrantedConnectionsFormPart(formState: IFormState): GrantedConnectionsFormPart { + return formState.getPart(DATA_CONTEXT_TEAM_FORM_CONNECTION_ACCESS_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const projectInfoResource = di.getService(ProjectInfoResource); + const teamsResource = di.getService(TeamsResource); + const graphQLService = di.getService(GraphQLService); + + return new GrantedConnectionsFormPart(formState, projectInfoResource, teamsResource, graphQLService); + }); +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUserList.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUserList.module.css new file mode 100644 index 0000000000..b808a2abc2 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUserList.module.css @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.group { + height: 100%; + + & .header { + flex: 0 0 auto; + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUserList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUserList.tsx new file mode 100644 index 0000000000..ddddcee906 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUserList.tsx @@ -0,0 +1,130 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useState } from 'react'; + +import { TeamRolesResource, UsersResource } from '@cloudbeaver/core-authentication'; +import { + Button, + Container, + getComputed, + getSelectedItems, + Group, + s, + Table, + TableBody, + TableColumnValue, + TableItem, + useObjectRef, + useResource, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import type { TLocalizationToken } from '@cloudbeaver/core-localization'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; + +import { getFilteredUsers } from './getFilteredUsers.js'; +import style from './GrantedUserList.module.css'; +import { GrantedUsersTableHeader, type IFilterState } from './GrantedUsersTableHeader/GrantedUsersTableHeader.js'; +import { GrantedUsersTableInnerHeader } from './GrantedUsersTableHeader/GrantedUsersTableInnerHeader.js'; +import { GrantedUsersTableItem } from './GrantedUsersTableItem.js'; +import type { IGrantedUser } from './IGrantedUser.js'; + +interface Props { + grantedUsers: IGrantedUser[]; + disabled: boolean; + onRevoke: (subjectIds: string[]) => void; + onTeamRoleAssign: (subjectId: string, teamRole: string | null) => void; + onEdit: () => void; +} + +export const GrantedUserList = observer(function GrantedUserList({ grantedUsers, disabled, onRevoke, onTeamRoleAssign, onEdit }) { + const styles = useS(style); + const props = useObjectRef({ onRevoke, onEdit }); + const translate = useTranslate(); + + const usersResource = useService(UsersResource); + const serverConfigResource = useService(ServerConfigResource); + + const teamRolesResource = useResource(GrantedUserList, TeamRolesResource, undefined); + + const [selectedSubjects] = useState>(() => observable(new Map())); + const [filterState] = useState(() => observable({ filterValue: '' })); + + const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); + + const users = getFilteredUsers(grantedUsers, filterState.filterValue) as IGrantedUser[]; + const keys = grantedUsers.map(user => user.userId); + + const revoke = useCallback(() => { + props.onRevoke(getSelectedItems(selectedSubjects)); + selectedSubjects.clear(); + }, []); + + let tableInfoText: TLocalizationToken | null = null; + if (!users.length) { + if (filterState.filterValue) { + tableInfoText = 'ui_search_no_result_placeholder'; + } else { + tableInfoText = 'ui_no_items_placeholder'; + } + } + + function isEditable(userId: string) { + if (serverConfigResource.distributed) { + return true; + } + + return !usersResource.isActiveUser(userId); + } + + return ( + + + + + + + + + + + isEditable(item)}> + 0} /> + + {tableInfoText && ( + + {translate(tableInfoText)} + + )} + {users.map(user => ( + + ))} + +
+
+
+ ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsers.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsers.module.css new file mode 100644 index 0000000000..fbccc496b0 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsers.module.css @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.box { + flex: 1; + height: 100%; + box-sizing: border-box; +} + +.placeholderBox { + max-height: 100%; + position: relative; + overflow: auto !important; +} + +.placeholder { + margin: 0 !important; +} + +.loader { + z-index: 2; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsers.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsers.tsx new file mode 100644 index 0000000000..16050c60cc --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsers.tsx @@ -0,0 +1,110 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import { useState } from 'react'; + +import { UsersResource, UsersResourceFilterKey } from '@cloudbeaver/core-authentication'; +import { Container, Group, InfoItem, Loader, s, TextPlaceholder, useAutoLoad, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { CachedResourceOffsetPageListKey } from '@cloudbeaver/core-resource'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; + +import type { TeamFormProps } from '../TeamsAdministrationFormService.js'; +import { GrantedUserList } from './GrantedUserList.js'; +import style from './GrantedUsers.module.css'; +import { GrantedUsersFormPart } from './GrantedUsersFormPart.js'; +import type { IGrantedUser } from './IGrantedUser.js'; +import { UserList } from './UserList.js'; + +export const GrantedUsers: TabContainerPanelComponent = observer(function GrantedUsers({ tabId, formState }) { + const styles = useS(style); + const translate = useTranslate(); + const [edit, setEdit] = useState(false); + + const tabState = useTabState(); + const { selected } = useTab(tabId); + + const serverConfigResource = useResource(UserList, ServerConfigResource, undefined, { active: selected }); + const isDefaultTeam = formState.state.teamId === serverConfigResource.data?.defaultUserTeam; + + const users = useResource(GrantedUsers, UsersResource, CachedResourceOffsetPageListKey(0, 1000).setParent(UsersResourceFilterKey()), { + active: selected && !isDefaultTeam, + }); + + const grantedUsers: IGrantedUser[] = []; + + for (const user of users.data) { + const granted = tabState.state.grantedUsers.find(grantedUser => grantedUser.userId === user?.userId); + + if (granted && user) { + grantedUsers.push({ + ...user, + teamRole: granted.teamRole, + }); + } + } + + function toggleEdit() { + setEdit(value => !value); + } + + useAutoLoad(GrantedUsers, tabState, selected && !tabState.isLoaded() && !isDefaultTeam); + + if (!selected) { + return null; + } + + if (isDefaultTeam) { + return ( + + + + {translate('plugin_authentication_administration_team_default_users_tooltip')} + + + + ); + } + + return ( + + {() => ( + + {!users.resource.values.length ? ( + + + {translate('administration_teams_team_granted_users_empty')} + + + ) : ( + <> + {formState.mode === 'edit' && tabState.isChanged && !formState.isDisabled && } + + + {edit && ( + user.userId)} + disabled={formState.isDisabled} + onGrant={tabState.grant} + /> + )} + + + )} + + )} + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersFormPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersFormPart.ts new file mode 100644 index 0000000000..cd4c3aaf2b --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersFormPart.ts @@ -0,0 +1,111 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { TeamRolesResource, TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { FormPart, formStatusContext, type IFormState } from '@cloudbeaver/core-ui'; + +import type { ITeamFormState } from '../TeamsAdministrationFormService.js'; +import type { IGrantedUsersState } from './IGrantedUsersState.js'; + +function getInitialState(): IGrantedUsersState { + return { + grantedUsers: [], + }; +} + +export class GrantedUsersFormPart extends FormPart { + constructor( + formState: IFormState, + private readonly teamsResource: TeamsResource, + private readonly usersResource: UsersResource, + private readonly teamRolesResource: TeamRolesResource, + ) { + super(formState, getInitialState()); + + this.grant = this.grant.bind(this); + this.revoke = this.revoke.bind(this); + this.assignTeamRole = this.assignTeamRole.bind(this); + } + + protected override async loader(): Promise { + if (this.formState.mode === 'edit' && this.formState.state.teamId) { + const grantedUsers = await this.teamsResource.loadGrantedUsers(this.formState.state.teamId); + + this.setInitialState({ + ...getInitialState(), + grantedUsers, + }); + + return; + } + + this.setInitialState(getInitialState()); + } + + protected override async saveChanges( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise { + const status = contexts.getContext(formStatusContext); + + if (!this.formState.state.teamId) { + return; + } + + const granted: string[] = []; + const revoked: string[] = []; + + const revokedUsers = this.initialState.grantedUsers.filter( + user => !this.state.grantedUsers.some(grantedUser => grantedUser.userId === user.userId), + ); + + for (const user of revokedUsers) { + await this.usersResource.revokeTeam(user.userId, this.formState.state.teamId); + revoked.push(user.userId); + } + + for (const user of this.state.grantedUsers) { + const initialUser = this.initialState.grantedUsers.find(grantedUser => grantedUser.userId === user.userId); + + if (!initialUser) { + await this.usersResource.grantTeam(user.userId, this.formState.state.teamId); + granted.push(user.userId); + } + + const initialRole = initialUser?.teamRole ?? null; + + if (user.teamRole !== initialRole) { + await this.teamRolesResource.assignTeamRoleToUser(user.userId, this.formState.state.teamId, user.teamRole); + } + } + + if (granted.length) { + status.info(`Added users: "${granted.join(', ')}"`); + } + + if (revoked.length) { + status.info(`Deleted users: "${revoked.join(', ')}"`); + } + } + + revoke(subjectIds: string[]) { + this.state.grantedUsers = this.state.grantedUsers.filter(subject => !subjectIds.includes(subject.userId)); + } + + grant(subjectIds: string[]) { + this.state.grantedUsers.push(...subjectIds.map(id => ({ userId: id, teamRole: null }))); + } + + assignTeamRole(subjectId: string, teamRole: string | null) { + const user = this.state.grantedUsers.find(user => user.userId === subjectId); + + if (user) { + user.teamRole = teamRole; + } + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTabService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTabService.ts new file mode 100644 index 0000000000..415671147b --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTabService.ts @@ -0,0 +1,39 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import React from 'react'; + +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; + +import { TeamsAdministrationFormService } from '../TeamsAdministrationFormService.js'; +import { getGrantedUsersFormPart } from './getGrantedUsersFormPart.js'; + +const GrantedUsers = React.lazy(async () => { + const { GrantedUsers } = await import('./GrantedUsers.js'); + return { default: GrantedUsers }; +}); + +@injectable(() => [TeamsAdministrationFormService]) +export class GrantedUsersTabService extends Bootstrap { + private readonly key: string; + + constructor(private readonly teamsAdministrationFormService: TeamsAdministrationFormService) { + super(); + this.key = 'granted-users'; + } + + override register(): void { + this.teamsAdministrationFormService.parts.add({ + key: this.key, + name: 'administration_teams_team_granted_users_tab_title', + title: 'administration_teams_team_granted_users_tab_title', + order: 2, + stateGetter: props => () => getGrantedUsersFormPart(props.formState), + panel: () => GrantedUsers, + }); + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.module.css new file mode 100644 index 0000000000..55d3274892 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.header { + composes: theme-border-color-background from global; + border-bottom: 1px solid; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.tsx new file mode 100644 index 0000000000..d124bcf62f --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableHeader.tsx @@ -0,0 +1,39 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Container, Filter, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; + +import style from './GrantedUsersTableHeader.module.css'; + +export interface IFilterState { + filterValue: string; +} + +interface Props extends React.PropsWithChildren { + filterState: IFilterState; + disabled: boolean; + className?: string; +} + +export const GrantedUsersTableHeader = observer(function GrantedUsersTableHeader({ filterState, disabled, className, children }) { + const styles = useS(style); + const translate = useTranslate(); + + return ( + + + {children} + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx new file mode 100644 index 0000000000..47c29cb055 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableHeader/GrantedUsersTableInnerHeader.tsx @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { TableColumnHeader, TableHeader, TableSelect, useTranslate } from '@cloudbeaver/core-blocks'; + +interface Props { + disabled?: boolean; + showUserTeamRole?: boolean; + className?: string; +} + +export const GrantedUsersTableInnerHeader = observer(function GrantedUsersTableInnerHeader({ disabled, showUserTeamRole, className }) { + const translate = useTranslate(); + + return ( + + + + + + {translate('administration_teams_team_granted_users_user_id')} + {showUserTeamRole && ( + + {translate('plugin_authentication_administration_team_user_team_role_supervisor')} + + )} + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableItem.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableItem.module.css new file mode 100644 index 0000000000..139e1f975d --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableItem.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.staticImage { + display: flex; + width: 24px; + min-width: 24px; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableItem.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableItem.tsx new file mode 100644 index 0000000000..f43388d8d1 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTableItem.tsx @@ -0,0 +1,61 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { USER_TEAM_ROLE_SUPERVISOR } from '@cloudbeaver/core-authentication'; +import { Checkbox, StaticImage, TableColumnValue, TableItem, TableItemSelect, useTranslate } from '@cloudbeaver/core-blocks'; + +import classes from './GrantedUsersTableItem.module.css'; + +interface Props { + id: any; + name: string; + icon: string; + disabled: boolean; + teamRole: string | null; + teamRoles: string[]; + iconTooltip?: string; + tooltip?: string; + onTeamRoleAssign: (subjectId: string, teamRole: string | null) => void; + className?: string; +} + +export const GrantedUsersTableItem = observer(function GrantedUsersTableItem({ + id, + name, + icon, + iconTooltip, + tooltip, + teamRole, + teamRoles, + onTeamRoleAssign, + disabled, + className, +}) { + const translate = useTranslate(); + + return ( + + + + + + + + {name} + {teamRoles.length > 0 && ( + + onTeamRoleAssign(id, value ? USER_TEAM_ROLE_SUPERVISOR : null)} + /> + + )} + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/IGrantedUser.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/IGrantedUser.ts new file mode 100644 index 0000000000..94c92e5e11 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/IGrantedUser.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; + +export interface IGrantedUser extends AdminUserInfoFragment { + teamRole: string | null; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/IGrantedUsersState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/IGrantedUsersState.ts new file mode 100644 index 0000000000..a47561c37f --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/IGrantedUsersState.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { schema } from '@cloudbeaver/core-utils'; + +const GRANTED_USER_STATE = schema.object({ + userId: schema.string(), + teamRole: schema.string().nullable(), +}); + +const GRANTED_USERS_STATE = schema.object({ + grantedUsers: schema.array(GRANTED_USER_STATE), +}); + +export type IGrantedUsersState = schema.infer; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UserList.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UserList.module.css new file mode 100644 index 0000000000..692f3ead06 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UserList.module.css @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.group { + height: 100%; +} + +.header { + flex: 0 0 auto; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UserList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UserList.tsx new file mode 100644 index 0000000000..271952c144 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UserList.tsx @@ -0,0 +1,108 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useState } from 'react'; + +import { UsersResource } from '@cloudbeaver/core-authentication'; +import { + Button, + Container, + getComputed, + getSelectedItems, + Group, + s, + Table, + TableBody, + TableColumnValue, + TableItem, + useObjectRef, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; + +import { getFilteredUsers } from './getFilteredUsers.js'; +import { GrantedUsersTableHeader, type IFilterState } from './GrantedUsersTableHeader/GrantedUsersTableHeader.js'; +import style from './UserList.module.css'; +import { UsersTableInnerHeader } from './UsersTableInnerHeader.js'; +import { UsersTableItem } from './UsersTableItem.js'; + +interface Props { + userList: AdminUserInfoFragment[]; + grantedUsers: string[]; + disabled: boolean; + onGrant: (subjectIds: string[]) => void; +} + +export const UserList = observer(function UserList({ userList, grantedUsers, disabled, onGrant }) { + const props = useObjectRef({ onGrant }); + const styles = useS(style); + const translate = useTranslate(); + + const usersResource = useService(UsersResource); + const serverConfigResource = useService(ServerConfigResource); + + const [selectedSubjects] = useState>(() => observable(new Map())); + const [filterState] = useState(() => observable({ filterValue: '' })); + + const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); + + const users = getFilteredUsers(userList, filterState.filterValue); + const keys = userList.map(user => user.userId); + + const grant = useCallback(() => { + props.onGrant(getSelectedItems(selectedSubjects)); + selectedSubjects.clear(); + }, []); + + function isEditable(userId: string) { + if (serverConfigResource.distributed) { + return true; + } + + return !usersResource.isActiveUser(userId); + } + + return ( + + + + + + + + isEditable(item) && !grantedUsers.includes(item)}> + + + {!users.length && filterState.filterValue && ( + + {translate('ui_search_no_result_placeholder')} + + )} + {users.map(user => ( + + ))} + +
+
+
+ ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UsersTableInnerHeader.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UsersTableInnerHeader.tsx new file mode 100644 index 0000000000..5b941f12d7 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UsersTableInnerHeader.tsx @@ -0,0 +1,29 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { TableColumnHeader, TableHeader, TableSelect, useTranslate } from '@cloudbeaver/core-blocks'; + +interface Props { + disabled?: boolean; + className?: string; +} + +export const UsersTableInnerHeader = observer(function UsersTableInnerHeader({ disabled, className }) { + const translate = useTranslate(); + + return ( + + + + + + {translate('administration_teams_team_granted_users_user_id')} + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UsersTableItem.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UsersTableItem.tsx new file mode 100644 index 0000000000..71c9f9d57c --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/UsersTableItem.tsx @@ -0,0 +1,36 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { StaticImage, TableColumnValue, TableItem, TableItemSelect } from '@cloudbeaver/core-blocks'; + +import style from './GrantedUsersTableItem.module.css'; + +interface Props { + id: any; + name: string; + icon: string; + disabled: boolean; + iconTooltip?: string; + tooltip?: string; + className?: string; +} + +export const UsersTableItem = observer(function UsersTableItem({ id, name, icon, iconTooltip, tooltip, disabled, className }) { + return ( + + + + + + + + {name} + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/getFilteredUsers.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/getFilteredUsers.ts similarity index 92% rename from webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/getFilteredUsers.ts rename to webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/getFilteredUsers.ts index fd38bb2e84..f770bec4db 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/getFilteredUsers.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/getFilteredUsers.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/getGrantedUsersFormPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/getGrantedUsersFormPart.ts new file mode 100644 index 0000000000..76b2820422 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/GrantedUsers/getGrantedUsersFormPart.ts @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { TeamRolesResource, TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import type { IFormState } from '@cloudbeaver/core-ui'; + +import type { ITeamFormState } from '../TeamsAdministrationFormService.js'; +import { GrantedUsersFormPart } from './GrantedUsersFormPart.js'; + +const DATA_CONTEXT_TEAM_FORM_GRANTED_USERS_PART = createDataContext('Team Form Granted Users Part'); + +export function getGrantedUsersFormPart(formState: IFormState): GrantedUsersFormPart { + return formState.getPart(DATA_CONTEXT_TEAM_FORM_GRANTED_USERS_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const teamsResource = di.getService(TeamsResource); + const usersResource = di.getService(UsersResource); + const teamRolesResource = di.getService(TeamRolesResource); + + return new GrantedUsersFormPart(formState, teamsResource, usersResource, teamRolesResource); + }); +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/ITeamOptionsState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/ITeamOptionsState.ts new file mode 100644 index 0000000000..b3decca015 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/ITeamOptionsState.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { schema } from '@cloudbeaver/core-utils'; + +const TEAM_OPTIONS_STATE = schema.object({ + teamId: schema.string(), + teamName: schema.string(), + teamPermissions: schema.array(schema.string()), + description: schema.string(), + metaParameters: schema.record(schema.string(), schema.any()), +}); + +export type ITeamOptionsState = schema.infer; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/Permissions.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/Permissions.tsx similarity index 80% rename from webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/Permissions.tsx rename to webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/Permissions.tsx index 5d80fc0391..77fff322b7 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/Permissions.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/Permissions.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -11,9 +11,14 @@ import { PermissionsResource } from '@cloudbeaver/core-administration'; import { FieldCheckbox, Group, GroupTitle, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import type { ITeamFormProps } from '../ITeamFormProps'; +import type { ITeamOptionsState } from './ITeamOptionsState.js'; -export const Permissions = observer(function Permissions({ state }) { +interface Props { + state: ITeamOptionsState; + disabled: boolean; +} + +export const Permissions = observer(function Permissions({ state, disabled }) { const translate = useTranslate(); const permissionsResource = useResource(Permissions, PermissionsResource, CachedMapAllKey); @@ -45,9 +50,9 @@ export const Permissions = observer(function Permissions({ state title={tooltip} label={label} name="teamPermissions" - state={state.config} - readOnly={state.readonly} - disabled={state.disabled} + state={state} + readOnly={disabled} + disabled={disabled} caption={caption} /> ); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamMetaParameters.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamMetaParameters.tsx new file mode 100644 index 0000000000..f2e5ffea6d --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamMetaParameters.tsx @@ -0,0 +1,34 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { TeamMetaParametersResource } from '@cloudbeaver/core-authentication'; +import { Group, GroupTitle, ObjectPropertyInfoForm, useResource, useTranslate } from '@cloudbeaver/core-blocks'; + +import type { ITeamOptionsState } from './ITeamOptionsState.js'; + +interface IProps { + state: ITeamOptionsState; + disabled: boolean; +} + +export const TeamMetaParameters = observer(function TeamMetaParameters({ state, disabled }) { + const teamMetaParameters = useResource(TeamMetaParameters, TeamMetaParametersResource, undefined); + const translate = useTranslate(); + + if (teamMetaParameters.data.length === 0) { + return null; + } + + return ( + + {translate('authentication_team_meta_parameters')} + + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptions.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptions.tsx new file mode 100644 index 0000000000..58569b862c --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptions.tsx @@ -0,0 +1,61 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Container, Group, InputField, Textarea, useAutoLoad, useCustomInputValidation, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; + +import type { TeamFormProps } from '../TeamsAdministrationFormService.js'; +import { Permissions } from './Permissions.js'; +import { TeamMetaParameters } from './TeamMetaParameters.js'; +import type { TeamOptionsFormPart } from './TeamOptionsFormPart.js'; +import { ENTITY_ID_VALIDATION } from '../../../shared/ENTITY_ID_VALIDATION.js'; + +export const TeamOptions: TabContainerPanelComponent = observer(function TeamOptions({ formState, tabId }) { + const serverConfigResource = useResource(TeamOptions, ServerConfigResource, undefined); + const tabState = useTabState(); + const translate = useTranslate(); + const edit = formState.mode === 'edit'; + const tab = useTab(tabId); + const loaded = tabState.isLoaded(); + + useAutoLoad(TeamOptions, tabState, tab.selected && !loaded); + + const idValidationRef = useCustomInputValidation(value => { + const v = value.trim(); + + if (!v) { + return translate('ui_field_is_required'); + } + + if (!v.match(ENTITY_ID_VALIDATION)) { + return translate('plugin_authentication_administration_team_id_validation_error'); + } + + return null; + }); + + return ( + + + + {translate('administration_teams_team_id')} + + + {translate('administration_teams_team_name')} + + + + {!serverConfigResource.resource.distributed && } + + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptionsFormPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptionsFormPart.ts new file mode 100644 index 0000000000..2099d270c7 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptionsFormPart.ts @@ -0,0 +1,132 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { TeamInfo, TeamInfoMetaParametersResource, TeamsResource } from '@cloudbeaver/core-authentication'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import type { LocalizationService } from '@cloudbeaver/core-localization'; +import { FormMode, FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui'; +import { getUniqueName } from '@cloudbeaver/core-utils'; + +import type { ITeamFormState } from '../TeamsAdministrationFormService.js'; +import type { ITeamOptionsState } from './ITeamOptionsState.js'; + +function getInitialState(): ITeamOptionsState { + return { + teamId: '', + teamName: '', + description: '', + teamPermissions: [], + metaParameters: {}, + }; +} + +export class TeamOptionsFormPart extends FormPart { + constructor( + formState: IFormState, + private readonly teamResource: TeamsResource, + private readonly teamsMetaParametersResource: TeamInfoMetaParametersResource, + private readonly localizationService: LocalizationService, + ) { + super(formState, getInitialState()); + } + + protected override async loader(): Promise { + if (this.formState.mode === 'edit' && this.formState.state.teamId) { + const [team, metaParameters] = await Promise.all([ + this.teamResource.load(this.formState.state.teamId), + this.teamsMetaParametersResource.load(this.formState.state.teamId), + ]); + + this.setInitialState({ + teamId: team.teamId, + teamName: team.teamName ?? '', + description: team.description ?? '', + teamPermissions: team.teamPermissions, + metaParameters: metaParameters, + }); + + return; + } + + this.setInitialState(getInitialState()); + } + + protected override validate( + data: IFormState, + contexts: IExecutionContextProvider>, + ): void | Promise { + const validation = contexts.getContext(formValidationContext); + + if (this.formState.mode === 'create') { + if (!this.state.teamId?.trim()) { + validation.error('administration_teams_team_info_id_invalid'); + } + + if (this.state.teamId && this.teamResource.has(this.state.teamId)) { + validation.error( + this.localizationService.translate('administration_teams_team_info_exists', undefined, { + teamId: this.state.teamId, + }), + ); + } + } + } + + protected override format(data: IFormState, contexts: IExecutionContextProvider>): void | Promise { + this.state.teamId = this.state.teamId.trim(); + + if (this.state.teamName) { + this.state.teamName = this.state.teamName.trim(); + + if (this.formState.mode === 'create') { + const teamNames = this.teamResource.values.map(team => team.teamName).filter(Boolean) as string[]; + this.state.teamName = getUniqueName(this.state.teamName, teamNames); + } + } + + if (this.state.description) { + this.state.description = this.state.description.trim(); + } + + if (this.state.metaParameters) { + for (const key of Object.keys(this.state.metaParameters)) { + if (typeof this.state.metaParameters[key] === 'string') { + this.state.metaParameters[key] = (this.state.metaParameters[key] as any).trim(); + } + } + } + } + + protected override async saveChanges( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise { + const teamInfo: TeamInfo = { + teamId: this.state.teamId, + teamName: this.state.teamName, + description: this.state.description, + teamPermissions: this.state.teamPermissions, + }; + + if (this.formState.mode === 'create') { + const team = await this.teamResource.createTeam(teamInfo); + this.formState.setMode(FormMode.Edit); + await this.teamsMetaParametersResource.setMetaParameters(this.state.teamId, this.state.metaParameters); + + this.formState.setState({ + teamId: team.teamId, + }); + + return; + } + + await Promise.all([ + this.teamResource.updateTeam(teamInfo), + this.teamsMetaParametersResource.setMetaParameters(this.state.teamId, this.state.metaParameters), + ]); + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptionsTabService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptionsTabService.ts new file mode 100644 index 0000000000..bbee444f24 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptionsTabService.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import React from 'react'; + +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; + +import { TeamsAdministrationFormService } from '../TeamsAdministrationFormService.js'; +import { getTeamOptionsFormPart } from './getTeamOptionsFormPart.js'; + +const TeamOptions = React.lazy(async () => { + const { TeamOptions } = await import('./TeamOptions.js'); + return { default: TeamOptions }; +}); + +@injectable(() => [TeamsAdministrationFormService]) +export class TeamOptionsTabService extends Bootstrap { + constructor(private readonly teamsAdministrationFormService: TeamsAdministrationFormService) { + super(); + } + + override register(): void { + this.teamsAdministrationFormService.parts.add({ + key: 'options', + name: 'ui_options', + order: 1, + stateGetter: props => () => getTeamOptionsFormPart(props.formState), + panel: () => TeamOptions, + }); + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/getTeamOptionsFormPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/getTeamOptionsFormPart.ts new file mode 100644 index 0000000000..9c02585f1b --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/getTeamOptionsFormPart.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { TeamInfoMetaParametersResource, TeamsResource } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import type { IFormState } from '@cloudbeaver/core-ui'; + +import type { ITeamFormState } from '../TeamsAdministrationFormService.js'; +import { TeamOptionsFormPart } from './TeamOptionsFormPart.js'; + +const DATA_CONTEXT_TEAM_FORM_OPTIONS_PART = createDataContext('Team Form Options Part'); + +export function getTeamOptionsFormPart(formState: IFormState): TeamOptionsFormPart { + return formState.getPart(DATA_CONTEXT_TEAM_FORM_OPTIONS_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const teamsResource = di.getService(TeamsResource); + const teamsMetaParametersResource = di.getService(TeamInfoMetaParametersResource); + const localizationService = di.getService(LocalizationService); + + return new TeamOptionsFormPart(formState, teamsResource, teamsMetaParametersResource, localizationService); + }); +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamForm.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamForm.module.css new file mode 100644 index 0000000000..b9090587dd --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamForm.module.css @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.topBar { + composes: theme-border-color-background from global; + margin-bottom: 16px; +} + +.topBar:before { + content: ''; + position: absolute; + bottom: 0; + width: 100%; + border-bottom: solid 2px; + border-color: inherit; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamForm.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamForm.tsx new file mode 100644 index 0000000000..0fd70d4b50 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamForm.tsx @@ -0,0 +1,89 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Button, Container, Form, s, StatusMessage, useForm, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; +import { FormMode, TabList, TabPanelList, TabsState } from '@cloudbeaver/core-ui'; +import { getFirstException } from '@cloudbeaver/core-utils'; + +import style from './TeamForm.module.css'; +import { TeamsAdministrationFormService } from './TeamsAdministrationFormService.js'; +import type { TeamsAdministrationFormState } from './TeamsAdministrationFormState.js'; + +interface Props { + state: TeamsAdministrationFormState; + onCancel?: () => void; + onSave?: VoidFunction; + className?: string; +} + +export const TeamForm = observer(function TeamForm({ state, onCancel, onSave = () => {}, className }) { + const styles = useS(style); + const service = useService(TeamsAdministrationFormService); + const notificationService = useService(NotificationService); + const translate = useTranslate(); + const editing = state.mode === 'edit'; + const form = useForm({ + onSubmit: async function onSubmit() { + const initialMode = state.mode; + const title = state.mode === 'create' ? 'administration_teams_team_info_created' : 'administration_teams_team_info_updated'; + + const saved = await state.save(); + const exception = getFirstException(state.exception); + + if (saved) { + const message = state.state.teamId ?? ''; + + notificationService.logSuccess({ title, message }); + + onSave?.(); + if (initialMode === FormMode.Create) { + onCancel?.(); + } + } else { + if (exception) { + const errorKey = state.mode === 'create' ? 'administration_teams_team_create_error' : 'administration_teams_team_save_error'; + notificationService.logException(exception, errorKey); + } + + // team created but other parts failed + if (initialMode === 'create' && state.mode === 'edit') { + notificationService.logSuccess({ title: 'administration_teams_team_info_created', message: state.state?.teamId ?? '' }); + } + } + }, + }); + + return ( +
+ + + + + + + + + + + + + + + + + +
+ ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamsAdministrationFormService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamsAdministrationFormService.ts new file mode 100644 index 0000000000..36631b9cc4 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamsAdministrationFormService.ts @@ -0,0 +1,24 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { FormBaseService, type IFormProps } from '@cloudbeaver/core-ui'; + +export interface ITeamFormState { + teamId: string | null; +} + +export type TeamFormProps = IFormProps; + +@injectable(() => [LocalizationService, NotificationService]) +export class TeamsAdministrationFormService extends FormBaseService { + constructor(localizationService: LocalizationService, notificationService: NotificationService) { + super(localizationService, notificationService, 'Administration Team form'); + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamsAdministrationFormState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamsAdministrationFormState.ts new file mode 100644 index 0000000000..b52fe23617 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/TeamsAdministrationFormState.ts @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IServiceProvider } from '@cloudbeaver/core-di'; +import { FormState } from '@cloudbeaver/core-ui'; + +import type { ITeamFormState, TeamsAdministrationFormService } from './TeamsAdministrationFormService.js'; + +export class TeamsAdministrationFormState extends FormState { + constructor(serviceProvider: IServiceProvider, service: TeamsAdministrationFormService, config: ITeamFormState) { + super(serviceProvider, service, config); + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/useTeamsAdministrationFormState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/useTeamsAdministrationFormState.ts new file mode 100644 index 0000000000..f1567ef57d --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/useTeamsAdministrationFormState.ts @@ -0,0 +1,36 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { useEffect, useRef } from 'react'; + +import { IServiceProvider, useService } from '@cloudbeaver/core-di'; + +import { TeamsAdministrationFormService } from './TeamsAdministrationFormService.js'; +import { TeamsAdministrationFormState } from './TeamsAdministrationFormState.js'; + +export function useTeamsAdministrationFormState(id: string | null, configure?: (state: TeamsAdministrationFormState) => any) { + const service = useService(TeamsAdministrationFormService); + const serviceProvider = useService(IServiceProvider); + const ref = useRef(null); + + if (ref.current?.state.teamId !== id) { + ref.current?.dispose(); + ref.current = new TeamsAdministrationFormState(serviceProvider, service, { + teamId: id, + }); + configure?.(ref.current); + } + + useEffect( + () => () => { + ref.current?.dispose(); + }, + [], + ); + + return ref.current; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsPage.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsPage.tsx deleted file mode 100644 index 29f7cd640c..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsPage.tsx +++ /dev/null @@ -1,85 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration'; -import { ColoredContainer, Container, Group, ToolsAction, ToolsPanel, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; - -import { CreateTeam } from './CreateTeam'; -import { CreateTeamService } from './CreateTeamService'; -import { TeamsTable } from './TeamsTable/TeamsTable'; -import { useTeamsTable } from './TeamsTable/useTeamsTable'; - -const styles = css` - ToolsPanel { - border-bottom: none; - } -`; - -interface Props { - sub?: IAdministrationItemSubItem; - param?: string | null; -} - -export const TeamsPage = observer(function TeamsPage({ sub, param }) { - const translate = useTranslate(); - const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES); - const service = useService(CreateTeamService); - - const table = useTeamsTable(); - const create = param === 'create'; - - return styled(style)( - - - - - {translate('ui_add')} - - - {translate('ui_refresh')} - - - {translate('ui_delete')} - - - - - - {create && ( - - - - )} - - - - - , - ); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeam.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeam.module.css new file mode 100644 index 0000000000..b44501f9d8 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeam.module.css @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.box { + height: 660px; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeam.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeam.tsx new file mode 100644 index 0000000000..f1c65299b6 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeam.tsx @@ -0,0 +1,38 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Container, Group, GroupTitle, Loader, s, Translate, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; + +import { TeamForm } from '../TeamsForm/TeamForm.js'; +import style from './CreateTeam.module.css'; +import { CreateTeamService } from './CreateTeamService.js'; + +export const CreateTeam: React.FC = observer(function CreateTeam() { + const translate = useTranslate(); + const styles = useS(style); + const service = useService(CreateTeamService); + + if (!service.data) { + return null; + } + + return ( + + + + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeamService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeamService.ts new file mode 100644 index 0000000000..a5effb299c --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeamService.ts @@ -0,0 +1,56 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 } from 'mobx'; + +import { injectable, IServiceProvider } from '@cloudbeaver/core-di'; + +import { TeamsAdministrationNavService } from '../TeamsAdministrationNavService.js'; +import { TeamsAdministrationFormService } from '../TeamsForm/TeamsAdministrationFormService.js'; +import { TeamsAdministrationFormState } from '../TeamsForm/TeamsAdministrationFormState.js'; + +@injectable(() => [TeamsAdministrationNavService, IServiceProvider, TeamsAdministrationFormService]) +export class CreateTeamService { + disabled = false; + data: TeamsAdministrationFormState | null; + + constructor( + private readonly teamsAdministrationNavService: TeamsAdministrationNavService, + private readonly serviceProvider: IServiceProvider, + private readonly service: TeamsAdministrationFormService, + ) { + this.data = null; + + this.cancelCreate = this.cancelCreate.bind(this); + this.create = this.create.bind(this); + + makeObservable(this, { + data: observable, + disabled: observable, + }); + } + + cancelCreate(): void { + this.teamsAdministrationNavService.navToRoot(); + } + + fillData(): void { + this.dispose(); + this.data = new TeamsAdministrationFormState(this.serviceProvider, this.service, { + teamId: null, + }); + } + + create(): void { + this.teamsAdministrationNavService.navToCreate(); + } + + dispose() { + this.data?.dispose(); + this.data = null; + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/Team.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/Team.module.css new file mode 100644 index 0000000000..890d9699c4 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/Team.module.css @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.expand { + cursor: pointer; +} + +.gap { + gap: 16px; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/Team.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/Team.tsx index 1fca98933d..cd6db0813c 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/Team.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/Team.tsx @@ -1,54 +1,36 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled, { css, use } from 'reshadow'; import type { TeamInfo } from '@cloudbeaver/core-authentication'; -import { Loader, Placeholder, TableColumnValue, TableItem, TableItemExpand, TableItemSelect, useStyles } from '@cloudbeaver/core-blocks'; +import { Link, Loader, Placeholder, s, TableColumnValue, TableItem, TableItemSelect, useS } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { TeamsAdministrationService } from '../TeamsAdministrationService'; -import { TeamEdit } from './TeamEdit'; - -const styles = css` - StaticImage { - display: flex; - width: 24px; - - &:not(:last-child) { - margin-right: 16px; - } - } - TableColumnValue[expand] { - cursor: pointer; - } - TableColumnValue[|gap] { - gap: 16px; - } -`; +import { TeamsAdministrationService } from '../TeamsAdministrationService.js'; +import style from './Team.module.css'; +import { TeamsTableOptionsPanelService } from './TeamsTableOptionsPanelService.js'; interface Props { team: TeamInfo; } export const Team = observer(function Team({ team }) { + const styles = useS(style); const service = useService(TeamsAdministrationService); + const teamsTableOptionsPanelService = useService(TeamsTableOptionsPanelService); - return styled(useStyles(styles))( - + return ( + - - - - - {team.teamId} + teamsTableOptionsPanelService.open(team.teamId)}> + {team.teamId} {team.teamName || ''} @@ -56,11 +38,11 @@ export const Team = observer(function Team({ team }) { {team.description || ''} - + - , + ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamEdit.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamEdit.tsx index fc8121f9d3..a19888d95e 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamEdit.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamEdit.tsx @@ -1,51 +1,66 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useCallback, useContext } from 'react'; -import styled, { css } from 'reshadow'; import { TeamsResource } from '@cloudbeaver/core-authentication'; -import { TableContext, useStyles } from '@cloudbeaver/core-blocks'; +import { ColoredContainer, ConfirmationDialog, GroupBack, GroupTitle, Text, useExecutor, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; +import { FormMode } from '@cloudbeaver/core-ui'; -import { TeamForm } from '../TeamForm'; -import { useTeamFormState } from '../useTeamFormState'; - -const styles = css` - box { - composes: theme-background-secondary theme-text-on-secondary from global; - box-sizing: border-box; - padding-bottom: 24px; - display: flex; - flex-direction: column; - height: 664px; - } -`; +import { TeamForm } from '../TeamsForm/TeamForm.js'; +import { useTeamsAdministrationFormState } from '../TeamsForm/useTeamsAdministrationFormState.js'; +import { TeamsTableOptionsPanelService } from './TeamsTableOptionsPanelService.js'; interface Props { item: string; + onClose: () => void; } export const TeamEdit = observer(function TeamEdit({ item }) { - const resource = useService(TeamsResource); - const tableContext = useContext(TableContext); + const translate = useTranslate(); + const teamsTableOptionsPanelService = useService(TeamsTableOptionsPanelService); + const commonDialogService = useService(CommonDialogService); + const team = useResource(TeamEdit, TeamsResource, item); - const collapse = useCallback(() => { - tableContext?.setItemExpand(item, false); - }, [tableContext, item]); + const formState = useTeamsAdministrationFormState(item, state => state.setMode(FormMode.Edit))!; - const data = useTeamFormState(resource, state => state.setOptions('edit')); + useExecutor({ + executor: teamsTableOptionsPanelService.onClose, + handlers: [ + async function closeHandler(event, contexts) { + if (formState.isChanged && event === 'before') { + const result = await commonDialogService.open(ConfirmationDialog, { + title: 'ui_save_reminder', + message: 'ui_are_you_sure', + confirmActionText: 'ui_yes', + }); - data.config.teamId = item; + if (result === DialogueStateResult.Rejected) { + ExecutorInterrupter.interrupt(contexts); + } + } + }, + ], + }); - return styled(useStyles(styles))( - - - , + return ( + + + + + {translate('ui_edit')} + {team.data?.teamName ? ` "${team.data.teamName}"` : ''} + + + + + ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsPage.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsPage.tsx new file mode 100644 index 0000000000..9d004ad6a4 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsPage.tsx @@ -0,0 +1,75 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { ColoredContainer, Container, Group, ToolsAction, ToolsPanel, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; + +import { CreateTeam } from './CreateTeam.js'; +import { CreateTeamService } from './CreateTeamService.js'; +import { TeamsTable } from './TeamsTable.js'; +import { useTeamsTable } from './useTeamsTable.js'; + +interface Props { + param?: string | null; +} + +export const TeamsPage = observer(function TeamsPage({ param }) { + const translate = useTranslate(); + const service = useService(CreateTeamService); + + const table = useTeamsTable(); + const create = param === 'create'; + + return ( + + + + + {translate('ui_create')} + + + {translate('ui_refresh')} + + + {translate('ui_delete')} + + + + + + {create && ( + + + + )} + + + + + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTable.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTable.module.css new file mode 100644 index 0000000000..f74cc01c48 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTable.module.css @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.error { + padding: 24px; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTable.tsx index 536a6e1df7..35d92b5967 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTable.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTable.tsx @@ -1,24 +1,39 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { css } from 'reshadow'; import type { TeamInfo } from '@cloudbeaver/core-authentication'; -import { Loader, Table, TableBody, TableColumnHeader, TableHeader, TableSelect, useTranslate } from '@cloudbeaver/core-blocks'; +import { + ExceptionMessageStyles, + Loader, + SContext, + type StyleRegistry, + Table, + TableBody, + TableColumnHeader, + TableHeader, + TableSelect, + useTranslate, +} from '@cloudbeaver/core-blocks'; import type { ILoadableState } from '@cloudbeaver/core-utils'; -import { Team } from './Team'; +import { Team } from './Team.js'; +import teamsTableStyle from './TeamsTable.module.css'; -const loaderStyle = css` - ExceptionMessage { - padding: 24px; - } -`; +const registry: StyleRegistry = [ + [ + ExceptionMessageStyles, + { + mode: 'append', + styles: [teamsTableStyle], + }, + ], +]; interface Props { teams: TeamInfo[]; @@ -32,24 +47,25 @@ export const TeamsTable = observer(function TeamsTable({ teams, state, se const keys = teams.map(team => team.teamId); return ( - - - - - - - - {translate('administration_teams_team_id')} - {translate('administration_teams_team_name')} - {translate('administration_teams_team_description')} - - - - {teams.map(team => ( - - ))} - -
-
+ + + + + + + + {translate('administration_teams_team_id')} + {translate('administration_teams_team_name')} + {translate('administration_teams_team_description')} + + + + {teams.map(team => ( + + ))} + +
+
+
); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTableOptionsPanel.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTableOptionsPanel.tsx new file mode 100644 index 0000000000..c5f8c6712d --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTableOptionsPanel.tsx @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; + +import { TeamEdit } from './TeamEdit.js'; +import { TeamsTableOptionsPanelService } from './TeamsTableOptionsPanelService.js'; + +export const TeamsTableOptionsPanel = observer(function TeamsTableOptionsPanel() { + const translate = useTranslate(); + const teamsTableOptionsPanelService = useService(TeamsTableOptionsPanelService); + + if (!teamsTableOptionsPanelService.itemId) { + return {translate('ui_not_found')}; + } + + return ; +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTableOptionsPanelService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTableOptionsPanelService.ts new file mode 100644 index 0000000000..6ecc5a89ff --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamsTableOptionsPanelService.ts @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { injectable } from '@cloudbeaver/core-di'; +import { BaseOptionsPanelService, OptionsPanelService } from '@cloudbeaver/core-ui'; + +const TeamsTableOptionsPanel = importLazyComponent(() => import('./TeamsTableOptionsPanel.js').then(m => m.TeamsTableOptionsPanel)); +const panelGetter = () => TeamsTableOptionsPanel; + +@injectable(() => [OptionsPanelService]) +export class TeamsTableOptionsPanelService extends BaseOptionsPanelService { + constructor(optionsPanelService: OptionsPanelService) { + super(optionsPanelService, panelGetter); + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/useTeamsTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/useTeamsTable.tsx index 1617e8cde3..bdb38b0038 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/useTeamsTable.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/useTeamsTable.tsx @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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, observable } from 'mobx'; -import { compareTeams, TeamInfo, TeamsResource } from '@cloudbeaver/core-authentication'; +import { compareTeams, type TeamInfo, TeamsResource } from '@cloudbeaver/core-authentication'; import { ConfirmationDialogDelete, TableState, useObservableRef, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; @@ -95,8 +95,9 @@ export function useTeamsTable(): Readonly { { processing: observable.ref, teams: computed, + state: observable.ref, }, - false, + { state: resource }, ['update', 'delete'], ); } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/useTeamFormState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/useTeamFormState.ts deleted file mode 100644 index e5a1295f91..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/useTeamFormState.ts +++ /dev/null @@ -1,28 +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 { useState } from 'react'; - -import type { TeamsResource } from '@cloudbeaver/core-authentication'; -import { useService } from '@cloudbeaver/core-di'; - -import type { ITeamFormState } from './ITeamFormProps'; -import { TeamFormService } from './TeamFormService'; -import { TeamFormState } from './TeamFormState'; - -export function useTeamFormState(resource: TeamsResource, configure?: (state: ITeamFormState) => any): ITeamFormState { - const service = useService(TeamFormService); - const [state] = useState(() => { - const state = new TeamFormState(service, resource); - configure?.(state); - - state.load(); - return state; - }); - - return state; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.m.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.m.css deleted file mode 100644 index cd77470f61..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.m.css +++ /dev/null @@ -1,45 +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. - */ - -.bar { - composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global; - position: relative; - display: flex; - margin-bottom: 16px; - - &:before { - content: ''; - position: absolute; - bottom: 0; - width: 100%; - border-bottom: solid 2px; - border-color: inherit; - } -} - -.tabList { - display: flex; - position: relative; - flex-shrink: 0; - align-items: center; -} - -.contentBox { - composes: theme-background-secondary theme-text-on-secondary theme-border-color-background from global; - display: flex; - flex: 1; - flex-direction: column; - overflow: auto; -} - -.submittingForm { - flex: 1; - overflow: auto; - display: flex; - flex-direction: column; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.module.css new file mode 100644 index 0000000000..d8633380d7 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.module.css @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.header { + composes: theme-border-color-background from global; + margin-bottom: 16px; + + &:before { + content: ''; + position: absolute; + bottom: 0; + width: 100%; + border-bottom: solid 2px; + border-color: inherit; + } +} + +.statusMessage { + margin-bottom: 8px; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx index f667f71f29..739de25c06 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,39 +10,32 @@ import { observer } from 'mobx-react-lite'; import { Button, Container, Form, s, StatusMessage, useAutoLoad, useForm, useS, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { - BASE_TAB_STYLES, - FormMode, - IFormState, - TabList, - TabPanelList, - TabsState, - UNDERLINE_TAB_BIG_STYLES, - UNDERLINE_TAB_STYLES, -} from '@cloudbeaver/core-ui'; +import { FormMode, type IFormState, TabList, TabPanelList, TabsState } from '@cloudbeaver/core-ui'; import { getFirstException } from '@cloudbeaver/core-utils'; -import style from './AdministrationUserForm.m.css'; -import { AdministrationUserFormService, IUserFormState } from './AdministrationUserFormService'; -import { DATA_CONTEXT_USER_FORM_INFO_PART } from './Info/DATA_CONTEXT_USER_FORM_INFO_PART'; +import style from './AdministrationUserForm.module.css'; +import { AdministrationUserFormDeleteButton } from './AdministrationUserFormDeleteButton.js'; +import { AdministrationUserFormService, type IUserFormState } from './AdministrationUserFormService.js'; +import { getUserFormInfoPart } from './Info/getUserFormInfoPart.js'; interface Props { state: IFormState; onClose: () => void; } -const deprecatedStyle = [BASE_TAB_STYLES, UNDERLINE_TAB_STYLES, UNDERLINE_TAB_BIG_STYLES]; export const AdministrationUserForm = observer(function AdministrationUserForm({ state, onClose }) { + const userFormInfoPart = getUserFormInfoPart(state); const styles = useS(style); const translate = useTranslate(); const notificationService = useService(NotificationService); const administrationUserFormService = useService(AdministrationUserFormService); + const editing = state.mode === FormMode.Edit; + const form = useForm({ async onSubmit() { const mode = state.mode; const saved = await state.save(); - const userFormInfoPart = state.dataContext.get(DATA_CONTEXT_USER_FORM_INFO_PART); if (saved) { if (mode === FormMode.Create) { @@ -66,22 +59,34 @@ export const AdministrationUserForm = observer(function AdministrationUse }, }); - useAutoLoad(AdministrationUserForm, state); + useAutoLoad(AdministrationUserForm, [userFormInfoPart]); return ( -
+ - - + + - - + + - - diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserFormDeleteButton.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserFormDeleteButton.tsx new file mode 100644 index 0000000000..1e435c92b3 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserFormDeleteButton.tsx @@ -0,0 +1,64 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { UsersResource } from '@cloudbeaver/core-authentication'; +import { Button, type ButtonProps, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; + +import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService.js'; +import { DeleteUserDialog } from './DeleteUserDialog.js'; +import { DisableUserDialog } from './DisableUserDialog.js'; + +interface Props extends ButtonProps { + userId: string; + enabled: boolean; + disableUser: () => Promise; +} + +export const AdministrationUserFormDeleteButton: React.FC = function AdministrationUserFormDeleteButton({ + userId, + enabled, + disableUser, + ...rest +}) { + const translate = useTranslate(); + const commonDialogService = useService(CommonDialogService); + const administrationUsersManagementService = useService(AdministrationUsersManagementService); + const usersResource = useService(UsersResource); + + const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled; + const deleteDisabled = usersResource.isActiveUser(userId) || userManagementDisabled; + + if (deleteDisabled) { + return null; + } + + async function openUserDeleteDialog() { + await commonDialogService.open(DeleteUserDialog, { + userId, + }); + } + + async function deleteUser() { + if (enabled) { + await commonDialogService.open(DisableUserDialog, { + userId, + onDelete: openUserDeleteDialog, + disableUser: disableUser, + }); + } else { + await openUserDeleteDialog(); + } + } + + return ( + + ); +}; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserFormService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserFormService.ts index 0b163cc47c..9413c041e2 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserFormService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserFormService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,7 +8,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { LocalizationService } from '@cloudbeaver/core-localization'; -import { FormBaseService, IFormProps } from '@cloudbeaver/core-ui'; +import { FormBaseService, type IFormProps } from '@cloudbeaver/core-ui'; export interface IUserFormState { userId: string | null; @@ -16,7 +16,7 @@ export interface IUserFormState { export type UserFormProps = IFormProps; -@injectable() +@injectable(() => [LocalizationService, NotificationService]) export class AdministrationUserFormService extends FormBaseService { constructor(localizationService: LocalizationService, notificationService: NotificationService) { super(localizationService, notificationService, 'Administration User form'); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserFormState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserFormState.ts index a4e02e430a..532980a93e 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserFormState.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserFormState.ts @@ -1,17 +1,17 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { App } from '@cloudbeaver/core-di'; +import type { IServiceProvider } from '@cloudbeaver/core-di'; import { FormState } from '@cloudbeaver/core-ui'; -import type { AdministrationUserFormService, IUserFormState } from './AdministrationUserFormService'; +import type { AdministrationUserFormService, IUserFormState } from './AdministrationUserFormService.js'; export class AdministrationUserFormState extends FormState { - constructor(app: App, service: AdministrationUserFormService, config: IUserFormState) { - super(app, service, config); + constructor(serviceProvider: IServiceProvider, service: AdministrationUserFormService, config: IUserFormState) { + super(serviceProvider, service, config); } } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART.ts deleted file mode 100644 index 450dc041dc..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART.ts +++ /dev/null @@ -1,27 +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 { UsersResource } from '@cloudbeaver/core-authentication'; -import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; -import { DATA_CONTEXT_FORM_STATE } from '@cloudbeaver/core-ui'; - -import type { AdministrationUserFormState } from '../AdministrationUserFormState'; -import { DATA_CONTEXT_USER_FORM_INFO_PART } from '../Info/DATA_CONTEXT_USER_FORM_INFO_PART'; -import { UserFormConnectionAccessPart } from './UserFormConnectionAccessPart'; - -export const DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART = createDataContext( - 'User Form Connection Access Part', - context => { - context.get(DATA_CONTEXT_USER_FORM_INFO_PART); // ensure that info part is loaded first - - const form = context.get(DATA_CONTEXT_FORM_STATE) as AdministrationUserFormState; - const di = context.get(DATA_CONTEXT_DI_PROVIDER); - const usersResource = di.getServiceByClass(UsersResource); - - return new UserFormConnectionAccessPart(form, usersResource); - }, -); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccess.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccess.tsx index f16458cf38..27d5c10b35 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccess.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccess.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { - ColoredContainer, + Container, Group, Table, TableBody, @@ -23,6 +23,7 @@ import { } from '@cloudbeaver/core-blocks'; import { compareConnectionsInfo, + ConnectionInfoOriginResource, ConnectionInfoProjectKey, ConnectionInfoResource, DBDriverResource, @@ -32,11 +33,11 @@ import { useService } from '@cloudbeaver/core-di'; import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, resourceKeyList } from '@cloudbeaver/core-resource'; import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; -import { isDefined } from '@cloudbeaver/core-utils'; +import { isDefined } from '@dbeaver/js-helpers'; -import type { UserFormProps } from '../AdministrationUserFormService'; -import type { UserFormConnectionAccessPart } from './UserFormConnectionAccessPart'; -import { UserFormConnectionTableItem } from './UserFormConnectionTableItem'; +import type { UserFormProps } from '../AdministrationUserFormService.js'; +import type { UserFormConnectionAccessPart } from './UserFormConnectionAccessPart.js'; +import { UserFormConnectionTableItem } from './UserFormConnectionTableItem.js'; export const UserFormConnectionAccess: TabContainerPanelComponent = observer(function UserFormConnectionAccess({ tabId }) { const translate = useTranslate(); @@ -44,16 +45,17 @@ export const UserFormConnectionAccess: TabContainerPanelComponent const driversResource = useService(DBDriverResource); const tabState = useTabState(); const projectLoader = useResource(UserFormConnectionAccess, ProjectInfoResource, CachedMapAllKey, { active: tab.selected }); - const connectionsLoader = useResource( - UserFormConnectionAccess, - ConnectionInfoResource, - ConnectionInfoProjectKey(...projectLoader.data.filter(isGlobalProject).map(project => project.id)), - { active: tab.selected }, - ); + const key = ConnectionInfoProjectKey(...projectLoader.data.filter(isGlobalProject).map(project => project.id)); + const connectionsLoader = useResource(UserFormConnectionAccess, ConnectionInfoResource, key, { active: tab.selected }); + const connectionsOriginsLoader = useResource(UserFormConnectionAccess, ConnectionInfoOriginResource, key, { active: tab.selected }); const connections = connectionsLoader.data.filter(isDefined).sort(compareConnectionsInfo); - const cloudExists = connections.some(isCloudConnection); - const localConnections = connections.filter(connection => !isCloudConnection(connection)); + const connectionsOrigins = connectionsOriginsLoader.data.filter(isDefined); + const cloudExists = connectionsOrigins.some(connectionOrigin => isCloudConnection(connectionOrigin.origin)); + const localConnectionsIds = new Set( + connectionsOrigins.filter(connection => !isCloudConnection(connection.origin)).map(connection => connection.id), + ); + const localConnections = connections.filter(connection => localConnectionsIds.has(connection.id)); useResource( UserFormConnectionAccess, @@ -68,11 +70,11 @@ export const UserFormConnectionAccess: TabContainerPanelComponent if (connections.length === 0) { return ( - + {translate('authentication_administration_user_connections_empty')} - + ); } @@ -92,9 +94,9 @@ export const UserFormConnectionAccess: TabContainerPanelComponent // } return ( - + {/* {info && } */} - + @@ -114,6 +116,6 @@ export const UserFormConnectionAccess: TabContainerPanelComponent
-
+
); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPanel.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPanel.tsx index e05e5063ee..9c70e08cc6 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPanel.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPanel.tsx @@ -1,18 +1,18 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { ColoredContainer, Group, TextPlaceholder, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks'; +import { Container, Group, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks'; import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; -import type { UserFormProps } from '../AdministrationUserFormService'; -import { DATA_CONTEXT_USER_FORM_INFO_PART } from '../Info/DATA_CONTEXT_USER_FORM_INFO_PART'; -import { UserFormConnectionAccess } from './UserFormConnectionAccess'; +import type { UserFormProps } from '../AdministrationUserFormService.js'; +import { getUserFormInfoPart } from '../Info/getUserFormInfoPart.js'; +import { UserFormConnectionAccess } from './UserFormConnectionAccess.js'; export const UserFormConnectionAccessPanel: TabContainerPanelComponent = observer(function UserFormConnectionAccessPanel({ tabId, @@ -21,7 +21,7 @@ export const UserFormConnectionAccessPanel: TabContainerPanelComponent - - {translate('connections_connection_access_admin_info')} - - + + {translate('connections_connection_access_admin_info')} + ); } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts index c1e59f6e08..c8cc58c306 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts @@ -1,25 +1,30 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { UsersResource } from '@cloudbeaver/core-authentication'; +import { isGlobalProject, type ProjectInfoResource } from '@cloudbeaver/core-projects'; import { type AdminConnectionGrantInfo, AdminSubjectType } from '@cloudbeaver/core-sdk'; -import { FormMode, FormPart } from '@cloudbeaver/core-ui'; +import { FormMode, FormPart, type IFormState } from '@cloudbeaver/core-ui'; import { isArraysEqual } from '@cloudbeaver/core-utils'; -import type { IUserFormState } from '../AdministrationUserFormService'; -import type { AdministrationUserFormState } from '../AdministrationUserFormState'; -import { DATA_CONTEXT_USER_FORM_INFO_PART } from '../Info/DATA_CONTEXT_USER_FORM_INFO_PART'; +import type { IUserFormState } from '../AdministrationUserFormService.js'; +import type { UserFormInfoPart } from '../Info/UserFormInfoPart.js'; export class UserFormConnectionAccessPart extends FormPart { - constructor(formState: AdministrationUserFormState, private readonly usersResource: UsersResource) { + constructor( + formState: IFormState, + private readonly usersResource: UsersResource, + private readonly projectInfoResource: ProjectInfoResource, + private readonly userFormInfoPart: UserFormInfoPart, + ) { super(formState, []); } - isChanged(): boolean { + override get isChanged(): boolean { if (!this.loaded) { return false; } @@ -55,20 +60,42 @@ export class UserFormConnectionAccessPart extends FormPart 0) { + await this.usersResource.deleteConnectionsAccess(globalProject.id, this.userFormInfoPart.state.userId, connectionsToRevoke); + } + + if (connectionsToGrant.length > 0) { + await this.usersResource.addConnectionsAccess(globalProject.id, this.userFormInfoPart.state.userId, connectionsToGrant); + } } private getGrantedConnections(state: AdminConnectionGrantInfo[]): string[] { return state.filter(connection => connection.subjectType !== AdminSubjectType.Team).map(connection => connection.dataSourceId); } + private getConnectionsDifferences(current: string[], next: string[]): { connectionsToRevoke: string[]; connectionsToGrant: string[] } { + const connectionsToRevoke = current.filter(subjectId => !next.includes(subjectId)); + const connectionsToGrant = next.filter(subjectId => !current.includes(subjectId)); + + return { connectionsToRevoke, connectionsToGrant }; + } + protected override async loader() { - const userFormInfoPart = this.formState.dataContext.get(DATA_CONTEXT_USER_FORM_INFO_PART); let grantedConnections: AdminConnectionGrantInfo[] = []; if (this.formState.mode === FormMode.Edit) { - grantedConnections = await this.usersResource.loadConnections(userFormInfoPart.initialState.userId); + grantedConnections = await this.usersResource.loadConnections(this.userFormInfoPart.initialState.userId); } this.setInitialState(grantedConnections); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPartBootstrap.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPartBootstrap.ts index d59b617916..d407d86aa6 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPartBootstrap.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPartBootstrap.ts @@ -1,25 +1,24 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 React from 'react'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; -import { AdministrationUserFormService } from '../AdministrationUserFormService'; -import { DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART } from './DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART'; +import { AdministrationUserFormService } from '../AdministrationUserFormService.js'; +import { getUserFormConnectionAccessPart } from './getUserFormConnectionAccessPart.js'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; -const UserFormConnectionAccessPanel = React.lazy(async () => { - const { UserFormConnectionAccessPanel } = await import('./UserFormConnectionAccessPanel'); - return { default: UserFormConnectionAccessPanel }; -}); +const UserFormConnectionAccessPanel = importLazyComponent(() => + import('./UserFormConnectionAccessPanel.js').then(m => m.UserFormConnectionAccessPanel), +); -@injectable() +@injectable(() => [AdministrationUserFormService, ProjectInfoResource]) export class UserFormConnectionAccessPartBootstrap extends Bootstrap { constructor( private readonly administrationUserFormService: AdministrationUserFormService, @@ -28,7 +27,7 @@ export class UserFormConnectionAccessPartBootstrap extends Bootstrap { super(); } - register(): void { + override register(): void { this.administrationUserFormService.parts.add({ key: 'connections_access', name: 'authentication_administration_user_connections_access', @@ -36,10 +35,8 @@ export class UserFormConnectionAccessPartBootstrap extends Bootstrap { order: 3, panel: () => UserFormConnectionAccessPanel, isHidden: () => !this.projectInfoResource.values.some(isGlobalProject), - stateGetter: props => () => props.formState.dataContext.get(DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART), + stateGetter: props => () => getUserFormConnectionAccessPart(props.formState), getLoader: () => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), }); } - - load(): void {} } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionTableItem.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionTableItem.module.css new file mode 100644 index 0000000000..8b50900944 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionTableItem.module.css @@ -0,0 +1,4 @@ +.staticImage { + width: 24px; + min-width: 24px; + } \ No newline at end of file diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionTableItem.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionTableItem.tsx index b93507e5a1..5e2fc0c787 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionTableItem.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionTableItem.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -13,7 +13,9 @@ import { useService } from '@cloudbeaver/core-di'; import { AdminSubjectType } from '@cloudbeaver/core-sdk'; import { useTabState } from '@cloudbeaver/core-ui'; -import type { UserFormConnectionAccessPart } from './UserFormConnectionAccessPart'; +import type { UserFormConnectionAccessPart } from './UserFormConnectionAccessPart.js'; + +import style from './UserFormConnectionTableItem.module.css'; interface Props { connection: Connection; @@ -45,7 +47,7 @@ export const UserFormConnectionTableItem = observer(function UserFormConn - + {connection.name} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/getUserFormConnectionAccessPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/getUserFormConnectionAccessPart.ts new file mode 100644 index 0000000000..b0ad3b350f --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/getUserFormConnectionAccessPart.ts @@ -0,0 +1,29 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { UsersResource } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import type { IFormState } from '@cloudbeaver/core-ui'; + +import type { IUserFormState } from '../AdministrationUserFormService.js'; +import { getUserFormInfoPart } from '../Info/getUserFormInfoPart.js'; +import { UserFormConnectionAccessPart } from './UserFormConnectionAccessPart.js'; + +const DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART = createDataContext('User Form Connection Access Part'); + +export function getUserFormConnectionAccessPart(formState: IFormState): UserFormConnectionAccessPart { + return formState.getPart(DATA_CONTEXT_USER_FORM_CONNECTION_ACCESS_PART, context => { + const userFormInfoPart = getUserFormInfoPart(formState); + + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const usersResource = di.getService(UsersResource); + const projectInfoResource = di.getService(ProjectInfoResource); + + return new UserFormConnectionAccessPart(formState, usersResource, projectInfoResource, userFormInfoPart); + }); +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/DeleteUserDialog.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/DeleteUserDialog.tsx new file mode 100644 index 0000000000..c7a2f633a7 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/DeleteUserDialog.tsx @@ -0,0 +1,82 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { useState } from 'react'; + +import { UsersResource } from '@cloudbeaver/core-authentication'; +import { + Button, + CommonDialogBody, + CommonDialogFooter, + CommonDialogHeader, + CommonDialogWrapper, + Container, + Fill, + InputField, + Text, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import type { DialogComponent } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; + +import { UsersTableOptionsPanelService } from '../UsersTable/UsersTableOptionsPanelService.js'; + +interface IPayload { + userId: string; +} + +export const DeleteUserDialog: DialogComponent = function DeleteUserDialog(props) { + const translate = useTranslate(); + const notificationService = useService(NotificationService); + const usersTableOptionsPanelService = useService(UsersTableOptionsPanelService); + const usersResource = useService(UsersResource); + + const [name, setName] = useState(''); + + async function deleteUser() { + try { + await usersTableOptionsPanelService.close(); + await usersResource.deleteUsers(props.payload.userId); + notificationService.logSuccess({ title: 'authentication_administration_users_delete_user_success', message: props.payload.userId }); + props.resolveDialog(); + } catch (exception: any) { + notificationService.logException(exception, 'authentication_administration_users_delete_user_fail'); + } + } + + return ( + + + + + {translate('authentication_administration_users_delete_user_info', undefined, { username: props.payload.userId })} + setName(String(v))} + /> + + + + + + + + + ); +}; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/DisableUserDialog.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/DisableUserDialog.module.css new file mode 100644 index 0000000000..13fe37039e --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/DisableUserDialog.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.commonDialogWrapper { + /* override because of the non-english locales and the medium size is too large */ + width: 440px !important; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/DisableUserDialog.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/DisableUserDialog.tsx new file mode 100644 index 0000000000..f3f55eab9a --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/DisableUserDialog.tsx @@ -0,0 +1,80 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { + Button, + CommonDialogBody, + CommonDialogFooter, + CommonDialogHeader, + CommonDialogWrapper, + Container, + Fill, + s, + Text, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import type { DialogComponent } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; + +import classes from './DisableUserDialog.module.css'; + +interface IPayload { + userId: string; + onDelete: () => void; + disableUser: () => Promise; +} + +export const DisableUserDialog: DialogComponent = observer(function DisableUserDialog(props) { + const translate = useTranslate(); + const styles = useS(classes); + const notificationService = useService(NotificationService); + + async function disableHandler() { + try { + await props.payload.disableUser(); + notificationService.logSuccess({ title: 'authentication_administration_users_disable_user_success', message: props.payload.userId }); + props.resolveDialog(); + } catch (exception: any) { + notificationService.logException(exception, 'authentication_administration_users_disable_user_fail'); + } + } + + function deleteHandler() { + props.payload.onDelete(); + props.rejectDialog(); + } + + return ( + + + + {translate('authentication_administration_users_delete_user_disable_info', undefined, { username: props.payload.userId })} + + + + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/DATA_CONTEXT_USER_FORM_INFO_PART.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/DATA_CONTEXT_USER_FORM_INFO_PART.ts deleted file mode 100644 index 930a29ba2c..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/DATA_CONTEXT_USER_FORM_INFO_PART.ts +++ /dev/null @@ -1,23 +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 { UsersResource } from '@cloudbeaver/core-authentication'; -import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { DATA_CONTEXT_FORM_STATE } from '@cloudbeaver/core-ui'; - -import type { AdministrationUserFormState } from '../AdministrationUserFormState'; -import { UserFormInfoPart } from './UserFormInfoPart'; - -export const DATA_CONTEXT_USER_FORM_INFO_PART = createDataContext('User Form Info Part', context => { - const form = context.get(DATA_CONTEXT_FORM_STATE) as AdministrationUserFormState; - const di = context.get(DATA_CONTEXT_DI_PROVIDER); - const usersResource = di.getServiceByClass(UsersResource); - const serverConfigResource = di.getServiceByClass(ServerConfigResource); - - return new UserFormInfoPart(serverConfigResource, form, usersResource); -}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/IUserFormInfoState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/IUserFormInfoState.ts index bce9d0749f..04f003e69d 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/IUserFormInfoState.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/IUserFormInfoState.ts @@ -1,15 +1,20 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 IUserFormInfoState { - userId: string; - enabled: boolean; - password: string; - metaParameters: Record; - teams: string[]; -} +import { schema } from '@cloudbeaver/core-utils'; + +export const USER_FORM_INFO_PART_SCHEMA = schema.object({ + userId: schema.string().trim(), + enabled: schema.boolean(), + password: schema.string().trim(), + metaParameters: schema.record(schema.string(), schema.string().trim().or(schema.any())), + teams: schema.array(schema.string()), + authRole: schema.string(), +}); + +export type IUserFormInfoState = schema.infer; 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..2351e1e66e 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 @@ -1,55 +1,61 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { ColoredContainer, Container, FieldCheckbox, Group, GroupTitle, Placeholder, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks'; +import { Container, FieldCheckbox, Group, GroupTitle, Placeholder, useAutoLoad, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; +import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; -import type { UserFormProps } from '../AdministrationUserFormService'; -import { UserFormInfoCredentials } from './UserFormInfoCredentials'; -import { UserFormInfoMetaParameters } from './UserFormInfoMetaParameters'; -import type { UserFormInfoPart } from './UserFormInfoPart'; -import { UserFormInfoPartService } from './UserFormInfoPartService'; -import { UserFormInfoTeams } from './UserFormInfoTeams'; +import { AdministrationUsersManagementService } from '../../../../AdministrationUsersManagementService.js'; +import type { UserFormProps } from '../AdministrationUserFormService.js'; +import { UserFormInfoCredentials } from './UserFormInfoCredentials.js'; +import { UserFormInfoMetaParameters } from './UserFormInfoMetaParameters.js'; +import type { UserFormInfoPart } from './UserFormInfoPart.js'; +import { UserFormInfoPartService } from './UserFormInfoPartService.js'; +import { UserFormInfoTeams } from './UserFormInfoTeams.js'; +import { UsersResource } from '@cloudbeaver/core-authentication'; +import { constructUserEnabledCaption } from './constructUserEnabledCaption.js'; export const UserFormInfo: TabContainerPanelComponent = observer(function UserFormInfo({ tabId, formState }) { const translate = useTranslate(); const tab = useTab(tabId); const tabState = useTabState(); const userFormInfoPartService = useService(UserFormInfoPartService); + const administrationUsersManagementService = useService(AdministrationUsersManagementService); + const userId = tabState.state.userId ?? formState.state.userId; + const user = useResource(UserFormInfo, UsersResource, userId, { + active: formState.mode === 'edit', + }); - useAutoLoad(UserFormInfo, tabState, tab.selected); + useAutoLoad(UserFormInfo, [tabState, ...administrationUsersManagementService.loaders], tab.selected); const disabled = tabState.isLoading(); - // let info: TLocalizationToken | null = null; - - // if (formState.mode === FormMode.Edit && tabState.isChanged()) { - // info = 'ui_save_reminder'; - // } + const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled; return ( - - {/* {info && } */} - - - - - - - {translate('authentication_user_status')} - - {translate('authentication_user_enabled')} - - - - - - + + + + + + + {translate('authentication_user_status')} + + + + + ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx index 1480c243d6..a2eedaf028 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx @@ -1,19 +1,28 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { isLocalUser, UsersResource } from '@cloudbeaver/core-authentication'; -import { Container, GroupTitle, InputField, useCustomInputValidation, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, isLocalUser, UsersResource } from '@cloudbeaver/core-authentication'; +import { + Container, + GroupTitle, + InputField, + useCustomInputValidation, + usePasswordValidation, + useResource, + useTranslate, +} from '@cloudbeaver/core-blocks'; import { FormMode } from '@cloudbeaver/core-ui'; import { isValuesEqual } from '@cloudbeaver/core-utils'; -import type { UserFormProps } from '../AdministrationUserFormService'; -import type { UserFormInfoPart } from './UserFormInfoPart'; +import type { UserFormProps } from '../AdministrationUserFormService.js'; +import type { UserFormInfoPart } from './UserFormInfoPart.js'; +import { ENTITY_ID_VALIDATION } from '../../shared/ENTITY_ID_VALIDATION.js'; const PASSWORD_PLACEHOLDER = '••••••'; @@ -26,13 +35,29 @@ interface Props extends UserFormProps { export const UserFormInfoCredentials = observer(function UserFormInfoCredentials({ formState, tabState, tabSelected, disabled }) { const translate = useTranslate(); const editing = formState.mode === FormMode.Edit; - const userInfo = useResource( - UserFormInfoCredentials, - UsersResource, - { key: tabState.initialState.userId, includes: ['includeMetaParameters'] }, - { active: tabSelected && editing }, - ); - const local = !editing || (userInfo.data && isLocalUser(userInfo.data)); + const userInfo = useResource(UserFormInfoCredentials, UsersResource, tabState.initialState.userId, { active: tabSelected && editing }); + const authProvidersResource = useResource(UserFormInfoCredentials, AuthProvidersResource, null); + const passwordValidationRef = usePasswordValidation(); + + let local = authProvidersResource.resource.isEnabled(AUTH_PROVIDER_LOCAL_ID); + + if (!local) { + local = !editing || (!!userInfo.data && isLocalUser(userInfo.data)); + } + + const usernameValidationRef = useCustomInputValidation(value => { + const v = value.trim(); + + if (!v) { + return translate('ui_field_is_required'); + } + + if (!v.match(ENTITY_ID_VALIDATION)) { + return translate('plugin_authentication_administration_user_username_validation_error'); + } + + return null; + }); const passwordRepeatRef = useCustomInputValidation(value => { if (!isValuesEqual(value, tabState.state.password, null)) { @@ -44,20 +69,30 @@ export const UserFormInfoCredentials = observer(function UserFormInfoCred return ( {translate('authentication_user_credentials')} - + {translate('authentication_user_name')} {local && ( <> (function UserFormInfoCred type="password" name="passwordRepeat" placeholder={editing ? PASSWORD_PLACEHOLDER : ''} - disabled={disabled} - mod="surface" + readOnly={disabled} required={!editing} canShowPassword keepSize diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoMetaParameters.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoMetaParameters.tsx index e44d7940bf..add6ff1a98 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoMetaParameters.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoMetaParameters.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,8 +10,8 @@ import { observer } from 'mobx-react-lite'; import { UserMetaParametersResource } from '@cloudbeaver/core-authentication'; import { Group, GroupTitle, ObjectPropertyInfoForm, useResource, useTranslate } from '@cloudbeaver/core-blocks'; -import type { UserFormProps } from '../AdministrationUserFormService'; -import type { UserFormInfoPart } from './UserFormInfoPart'; +import type { UserFormProps } from '../AdministrationUserFormService.js'; +import type { UserFormInfoPart } from './UserFormInfoPart.js'; interface Props extends UserFormProps { tabState: UserFormInfoPart; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts index 0bc19ff03e..271bc9074f 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPart.ts @@ -1,54 +1,62 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; +import { observable, toJS } from 'mobx'; -import type { AdminUser, UsersResource } from '@cloudbeaver/core-authentication'; +import type { AdminUser, AuthRolesResource, UserMetaParameter, UsersMetaParametersResource, UsersResource } from '@cloudbeaver/core-authentication'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { getCachedDataResourceLoaderState } from '@cloudbeaver/core-resource'; import type { ServerConfigResource } from '@cloudbeaver/core-root'; -import { FormMode, FormPart, formValidationContext, IFormState } from '@cloudbeaver/core-ui'; -import { isArraysEqual, isDefined, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils'; -import { DATA_CONTEXT_LOADABLE_STATE } from '@cloudbeaver/core-view'; +import { FormMode, FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui'; +import { isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils'; +import { isDefined } from '@dbeaver/js-helpers'; -import type { IUserFormState } from '../AdministrationUserFormService'; -import type { AdministrationUserFormState } from '../AdministrationUserFormState'; -import type { IUserFormInfoState } from './IUserFormInfoState'; +import type { IUserFormState } from '../AdministrationUserFormService.js'; +import { USER_FORM_INFO_PART_SCHEMA, type IUserFormInfoState } from './IUserFormInfoState.js'; const DEFAULT_ENABLED = true; export class UserFormInfoPart extends FormPart { constructor( + private readonly authRolesResource: AuthRolesResource, private readonly serverConfigResource: ServerConfigResource, - formState: AdministrationUserFormState, + formState: IFormState, private readonly usersResource: UsersResource, + private readonly usersMetaParametersResource: UsersMetaParametersResource, ) { - super(formState, { - userId: formState.state.userId || '', - enabled: DEFAULT_ENABLED, - password: '', - metaParameters: {}, - teams: [], - }); + super( + formState, + { + userId: formState.state.userId || '', + enabled: DEFAULT_ENABLED, + password: '', + metaParameters: {}, + teams: [], + authRole: '', + }, + USER_FORM_INFO_PART_SCHEMA, + ); + + this.disableUser = this.disableUser.bind(this); } - isOutdated(): boolean { + override isOutdated(): boolean { if (this.formState.mode === FormMode.Edit && this.initialState.userId) { - return this.usersResource.isOutdated(this.initialState.userId); + return this.usersResource.isOutdated(this.initialState.userId) || this.usersMetaParametersResource.isOutdated(this.initialState.userId); } - return false; + return this.serverConfigResource.isOutdated() || this.authRolesResource.isOutdated(); } - isLoaded(): boolean { + override isLoaded(): boolean { if ( this.formState.mode === FormMode.Edit && this.initialState.userId && - !this.usersResource.isLoaded(this.initialState.userId, ['includeMetaParameters']) + !this.usersResource.isLoaded(this.initialState.userId) && + !this.usersMetaParametersResource.isLoaded(this.initialState.userId) ) { return false; } @@ -56,7 +64,13 @@ export class UserFormInfoPart extends FormPart [getCachedDataResourceLoaderState(this.serverConfigResource, undefined)]); + if (this.authRolesResource.data.length > 0) { + const authRole = getTransformedAuthRole(this.state.authRole); + if (!authRole || !this.authRolesResource.data.includes(authRole)) { + validation.error('authentication_user_role_not_set'); + } + } } private async updateCredentials() { - if (this.state.password) { + const password = this.state.password; + + if (password) { await this.usersResource.updateCredentials(this.state.userId, { profile: '0', - credentials: { password: this.state.password }, + credentials: { password }, }); } } + private async updateAuthRole() { + if (this.state.userId && this.authRolesResource.data.length > 0) { + const authRole = getTransformedAuthRole(this.state.authRole); + const user = this.usersResource.get(this.state.userId); + + if (!isValuesEqual(authRole, user?.authRole, '')) { + await this.usersResource.setAuthRole(this.state.userId, authRole, true); + } + } + } + private async updateTeams() { let grantedTeams: string[] = []; if (this.state.userId) { - grantedTeams = this.usersResource.get(this.state.userId)?.grantedTeams ?? []; + grantedTeams = toJS(this.usersResource.get(this.state.userId)?.grantedTeams ?? []); } if (isArraysEqual(this.state.teams, grantedTeams)) { @@ -158,31 +195,43 @@ export class UserFormInfoPart extends FormPart { - const { UserFormInfo } = await import('./UserFormInfo'); - return { default: UserFormInfo }; -}); +const UserFormInfo = importLazyComponent(() => import('./UserFormInfo.js').then(m => m.UserFormInfo)); -@injectable() +@injectable(() => [AdministrationUserFormService]) export class UserFormInfoPartBootstrap extends Bootstrap { constructor(private readonly administrationUserFormService: AdministrationUserFormService) { super(); } - register(): void { + override register(): void { this.administrationUserFormService.parts.add({ key: 'info', name: 'authentication_administration_user_info', title: 'authentication_administration_user_info', order: 1, panel: () => UserFormInfo, - stateGetter: props => () => props.formState.dataContext.get(DATA_CONTEXT_USER_FORM_INFO_PART), + stateGetter: props => () => getUserFormInfoPart(props.formState), }); } - - load(): void {} } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPartService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPartService.ts index 5f3c10c109..886abc1c1b 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPartService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoPartService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,7 +8,7 @@ import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; -import type { UserFormProps } from '../AdministrationUserFormService'; +import type { UserFormProps } from '../AdministrationUserFormService.js'; @injectable() export class UserFormInfoPartService { diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoTeams.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoTeams.tsx index 7a4a589e3f..e4ca4aab4e 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoTeams.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoTeams.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,10 +10,11 @@ import { observer } from 'mobx-react-lite'; import { compareTeams, TeamsResource } from '@cloudbeaver/core-authentication'; import { FieldCheckbox, Group, GroupTitle, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { isDefined } from '@cloudbeaver/core-utils'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { isDefined } from '@dbeaver/js-helpers'; -import type { UserFormProps } from '../AdministrationUserFormService'; -import type { UserFormInfoPart } from './UserFormInfoPart'; +import type { UserFormProps } from '../AdministrationUserFormService.js'; +import type { UserFormInfoPart } from './UserFormInfoPart.js'; interface Props extends UserFormProps { tabState: UserFormInfoPart; @@ -23,27 +24,33 @@ interface Props extends UserFormProps { export const UserFormInfoTeams = observer(function UserFormInfoTeams({ formState, tabState, tabSelected, disabled }) { const translate = useTranslate(); + const serverConfigResource = useResource(UserFormInfoTeams, ServerConfigResource, undefined); const teamsLoader = useResource(UserFormInfoTeams, TeamsResource, CachedMapAllKey, { active: tabSelected }); const teams = teamsLoader.data.filter(isDefined).sort(compareTeams); + const defaultTeam = serverConfigResource.data?.defaultUserTeam; + return ( <> {translate('authentication_user_team')} - + {teams.map(team => { + const isDefault = team.teamId === defaultTeam; const label = `${team.teamId}${team.teamName && team.teamName !== team.teamId ? ' (' + team.teamName + ')' : ''}`; const tooltip = `${label}${team.description ? '\n' + team.description : ''}`; + return ( - {label} - + /> ); })} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/constructUserEnabledCaption.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/constructUserEnabledCaption.ts new file mode 100644 index 0000000000..38ff2c8539 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/constructUserEnabledCaption.ts @@ -0,0 +1,31 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; +import { isNotNullDefined } from '@dbeaver/js-helpers'; + +export function constructUserEnabledCaption(user: AdminUserInfoFragment | undefined): string { + if (!isNotNullDefined(user) || user.enabled) { + return ''; + } + + let caption = ''; + + if (user.disableReason) { + caption += user.disableReason; + } + + if (user.disabledBy) { + caption += ` (${user.disabledBy})`; + } + + if (user.disableDate) { + caption += `, ${new Date(user.disableDate).toLocaleDateString()}`; + } + + return caption; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/getUserFormInfoPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/getUserFormInfoPart.ts new file mode 100644 index 0000000000..eb5080a360 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/getUserFormInfoPart.ts @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { AuthRolesResource, UsersMetaParametersResource, UsersResource } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import type { IFormState } from '@cloudbeaver/core-ui'; + +import type { IUserFormState } from '../AdministrationUserFormService.js'; +import { UserFormInfoPart } from './UserFormInfoPart.js'; + +const DATA_CONTEXT_USER_FORM_INFO_PART = createDataContext('User Form Info Part'); + +export function getUserFormInfoPart(formState: IFormState): UserFormInfoPart { + return formState.getPart(DATA_CONTEXT_USER_FORM_INFO_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const usersResource = di.getService(UsersResource); + const serverConfigResource = di.getService(ServerConfigResource); + const authRolesResource = di.getService(AuthRolesResource); + const usersMetaParametersResource = di.getService(UsersMetaParametersResource); + + return new UserFormInfoPart(authRolesResource, serverConfigResource, formState, usersResource, usersMetaParametersResource); + }); +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginInfoPanel.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginInfoPanel.tsx index b65cf39d5d..db6103f72f 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginInfoPanel.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginInfoPanel.tsx @@ -1,168 +1,133 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; +import { Fragment } from 'react'; -import { UsersResource } from '@cloudbeaver/core-authentication'; +import { type AdminUserOrigin, UsersOriginDetailsResource, UsersResource } from '@cloudbeaver/core-authentication'; import { - ColoredContainer, - ExceptionMessage, + Button, + Select, + ConfirmationDialog, + Container, Group, - IAutoLoadable, - Loader, + GroupItem, ObjectPropertyInfoForm, - TextPlaceholder, - useAutoLoad, - useObjectRef, + useObservableRef, useResource, useTranslate, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import type { AdminUserInfo, ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; -import { FormMode, TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { FormMode, type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; -import type { UserFormProps } from '../AdministrationUserFormService'; -import { getUserFormOriginTabId } from './getUserFormOriginTabId'; - -interface IInnerState extends IAutoLoadable { - state: IState; - user: AdminUserInfo | undefined; -} +import type { UserFormProps } from '../AdministrationUserFormService.js'; interface IState { - properties: ObjectPropertyInfo[]; - state: Record; - loading: boolean; - loaded: boolean; - exception: Error | null; + selectedOrigin: string; } +const empty: any[] = []; + export const UserFormOriginInfoPanel: TabContainerPanelComponent = observer(function UserFormOriginInfoPanel({ tabId, formState: { mode, state }, }) { const translate = useTranslate(); - const usersResource = useService(UsersResource); - const localState = useTabState(() => ({ - origin: null, - properties: [], - state: {}, - loading: false, - loaded: false, - exception: null, - })); const editing = mode === FormMode.Edit; - const userInfo = useResource(UserFormOriginInfoPanel, UsersResource, state.userId, { active: editing }); - let origin = userInfo.data?.origins.find(origin => getUserFormOriginTabId('origin', origin) === tabId); - - if (!origin) { - origin = userInfo.data?.origins[0]; - } - - const loadableState = useObjectRef( + const localState = useObservableRef( () => ({ - get exception(): Error | null { - return this.state.exception; - }, - isError(): boolean { - return !!this.state.exception; - }, - isLoaded(): boolean { - return this.state.loaded; - }, - isLoading(): boolean { - return this.state.loading; - }, - async load(reload = false) { - if ((this.state.loaded && !reload) || this.state.loading || !this.user) { - return; - } - - this.state.loading = true; - this.state.exception = null; - - try { - usersResource.markOutdated(this.user.userId); - const userOrigin = await usersResource.load(this.user.userId, ['customIncludeOriginDetails']); - - let origin = userOrigin.origins.find(origin => getUserFormOriginTabId('origin', origin) === tabId); - - if (!origin) { - origin = this.user.origins[0]; - } - - const propertiesState = {} as Record; - - for (const property of origin.details!) { - propertiesState[property.id!] = property.value; - } - this.state.properties = origin.details!; - this.state.state = propertiesState; - this.state.loaded = true; - } catch (error: any) { - this.state.exception = error; - } finally { - this.state.loading = false; - } - }, - async reload() { - await this.load(); - }, + selectedOrigin: '0', }), { - state: localState, - user: userInfo.data as AdminUserInfo | undefined, + selectedOrigin: observable.ref, }, - ['reload', 'load', 'isLoaded', 'isLoading', 'isError'], + false, ); + const userInfoLoader = useResource(UserFormOriginInfoPanel, UsersResource, state.userId, { + active: editing, + }); + const commonDialogService = useService(CommonDialogService); + const notificationService = useService(NotificationService); + const origins = userInfoLoader.data?.origins ?? []; + const origin: AdminUserOrigin | undefined = origins[localState.selectedOrigin as any]; + const usersOriginDetailsResource = useResource(UserFormOriginInfoPanel, UsersOriginDetailsResource, state.userId, { + active: editing, + }); + const originDetails = usersOriginDetailsResource.data?.origins?.[localState.selectedOrigin as any]?.details ?? []; const { selected } = useTab(tabId); - useAutoLoad(UserFormOriginInfoPanel, loadableState, selected); - if (!selected) { return null; } - if (localState.loading) { - return ( - - - - - - ); - } - - if (localState.exception) { - return ( - - - loadableState.reload?.()} /> - - - ); + if (!origin && origins.length > 0) { + localState.selectedOrigin = '0'; } - if (!origin || (localState.loaded && localState.properties.length === 0)) { - return ( - - - {translate('authentication_administration_user_origin_empty')} - - - ); + async function deleteHandler() { + const result = await commonDialogService.open(ConfirmationDialog, { + title: 'ui_data_delete_confirmation', + message: translate('authentication_administration_user_delete_credentials_confirmation_message', undefined, { + originName: origin?.displayName, + userId: state.userId, + }), + confirmActionText: 'ui_delete', + }); + + if (result !== DialogueStateResult.Rejected) { + try { + await userInfoLoader.resource.deleteCredentials(state.userId!, origin!.type!); + notificationService.logSuccess({ title: 'authentication_administration_user_delete_credentials_success' }); + } catch (exception: any) { + notificationService.logException(exception, 'authentication_administration_user_delete_credentials_error'); + } + } } return ( - - - + + + + {origins.length === 0 && {translate('authentication_administration_user_auth_methods_empty')}} + {origin && ( + + + + + + + + + + + )} - + ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginInfoTab.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginInfoTab.tsx index a58a6d0ccd..c4f82b323d 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginInfoTab.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginInfoTab.tsx @@ -1,34 +1,23 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled from 'reshadow'; -import { UsersResource } from '@cloudbeaver/core-authentication'; -import { Translate, useResource, useStyles } from '@cloudbeaver/core-blocks'; -import { FormMode, Tab, TabContainerTabComponent, TabTitle } from '@cloudbeaver/core-ui'; +import { Translate } from '@cloudbeaver/core-blocks'; +import { Tab, type TabContainerTabComponent, TabTitle } from '@cloudbeaver/core-ui'; -import type { UserFormProps } from '../AdministrationUserFormService'; -import { getUserFormOriginTabId } from './getUserFormOriginTabId'; +import type { UserFormProps } from '../AdministrationUserFormService.js'; -export const UserFormOriginInfoTab: TabContainerTabComponent = observer(function UserFormOriginInfoTab({ - tabId, - formState: { mode, state }, - style, - ...rest -}) { - const editing = mode === FormMode.Edit; - const userInfo = useResource(UserFormOriginInfoTab, UsersResource, state.userId, { active: editing }); - const origin = userInfo.data?.origins.find(origin => getUserFormOriginTabId('origin', origin) === tabId); - return styled(useStyles(style))( - +export const UserFormOriginInfoTab: TabContainerTabComponent = observer(function UserFormOriginInfoTab(props) { + return ( + - + - , + ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginPartBootstrap.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginPartBootstrap.ts index 8db233749a..46724c7b28 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginPartBootstrap.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/UserFormOriginPartBootstrap.ts @@ -1,65 +1,40 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import React from 'react'; -import { AUTH_PROVIDER_LOCAL_ID, UsersResource } from '@cloudbeaver/core-authentication'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { FormMode } from '@cloudbeaver/core-ui'; -import { AdministrationUserFormService } from '../AdministrationUserFormService'; -import { DATA_CONTEXT_USER_FORM_INFO_PART } from '../Info/DATA_CONTEXT_USER_FORM_INFO_PART'; -import { getUserFormOriginTabId } from './getUserFormOriginTabId'; +import { AdministrationUserFormService } from '../AdministrationUserFormService.js'; const UserFormOriginInfoPanel = React.lazy(async () => { - const { UserFormOriginInfoPanel } = await import('./UserFormOriginInfoPanel'); + const { UserFormOriginInfoPanel } = await import('./UserFormOriginInfoPanel.js'); return { default: UserFormOriginInfoPanel }; }); const UserFormOriginInfoTab = React.lazy(async () => { - const { UserFormOriginInfoTab } = await import('./UserFormOriginInfoTab'); + const { UserFormOriginInfoTab } = await import('./UserFormOriginInfoTab.js'); return { default: UserFormOriginInfoTab }; }); -@injectable() +@injectable(() => [AdministrationUserFormService]) export class UserFormOriginPartBootstrap extends Bootstrap { - constructor(private readonly administrationUserFormService: AdministrationUserFormService, private readonly usersResource: UsersResource) { + constructor(private readonly administrationUserFormService: AdministrationUserFormService) { super(); } - register(): void { + override register(): void { this.administrationUserFormService.parts.add({ key: 'origin', order: 2, - generator: (tabId, props) => { - const userFormInfoPart = props?.formState.dataContext.get(DATA_CONTEXT_USER_FORM_INFO_PART); - if (props?.formState?.mode === FormMode.Edit && userFormInfoPart?.initialState.userId) { - const user = this.usersResource.get(userFormInfoPart.initialState.userId); - const origins = user?.origins.filter(origin => origin.type !== AUTH_PROVIDER_LOCAL_ID); - - if (origins && origins.length > 0) { - return origins.map(origin => getUserFormOriginTabId(tabId, origin)); - } - } - - return ['origin']; - }, - isHidden: (tabId, props) => { - const userFormInfoPart = props?.formState.dataContext.get(DATA_CONTEXT_USER_FORM_INFO_PART); - if (props?.formState?.mode === FormMode.Edit && userFormInfoPart?.initialState.userId) { - const user = this.usersResource.get(userFormInfoPart.initialState.userId); - return !user?.origins.some(origin => origin.type !== AUTH_PROVIDER_LOCAL_ID); - } - return true; - }, + isHidden: (tabId, props) => props?.formState?.mode !== FormMode.Edit, panel: () => UserFormOriginInfoPanel, tab: () => UserFormOriginInfoTab, }); } - - load(): void {} } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/getUserFormOriginTabId.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/getUserFormOriginTabId.ts index 683f43bf56..c496770782 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/getUserFormOriginTabId.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Origin/getUserFormOriginTabId.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserFormBaseBootstrap.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserFormBaseBootstrap.ts index d8389abc61..4096cf21dd 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserFormBaseBootstrap.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserFormBaseBootstrap.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -12,8 +12,4 @@ export class UserFormBaseBootstrap extends Bootstrap { constructor() { super(); } - - register(): void | Promise {} - - load(): void | Promise {} } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministration.m.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministration.m.css deleted file mode 100644 index 822d95bd7e..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministration.m.css +++ /dev/null @@ -1,29 +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. - */ - -.tabList { - position: relative; - flex-shrink: 0; - align-items: center; - padding: 0 24px; - - .tab { - height: 100%; - font-size: 14px; - font-weight: 700; - padding: 0 4px; - - .tabTitle { - font-weight: 700; - } - } -} - -.tabPanel { - overflow: auto; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministration.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministration.tsx index 799e2d1717..847eae7ba5 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministration.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministration.tsx @@ -1,40 +1,50 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled, { css } from 'reshadow'; -import { ADMINISTRATION_TOOLS_PANEL_STYLES, AdministrationItemContentComponent } from '@cloudbeaver/core-administration'; -import { s, ToolsPanel, useS, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import type { AdministrationItemContentComponent } from '@cloudbeaver/core-administration'; +import { s, SContext, type StyleRegistry, ToolsPanel, useS, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { BASE_TAB_STYLES, ITabData, Tab, TabList, TabPanel, TabsState, TabTitle, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui'; +import { + type ITabData, + Tab, + TabList, + TabPanel, + TabPanelList, + TabPanelStyles, + TabsState, + TabStyles, + TabTitle, + TabTitleStyles, +} from '@cloudbeaver/core-ui'; -import { TeamsPage } from './Teams/TeamsPage'; -import style from './UsersAdministration.m.css'; -import { EUsersAdministrationSub, UsersAdministrationNavigationService } from './UsersAdministrationNavigationService'; -import { UsersPage } from './UsersTable/UsersPage'; +import style from './shared/UsersAdministration.module.css'; +import tabStyle from './shared/UsersAdministrationTab.module.css'; +import tabPanelStyle from './shared/UsersAdministrationTabPanel.module.css'; +import TabTitleModuleStyles from './shared/UsersAdministrationTabTitle.module.css'; +import { TeamsPage } from './Teams/TeamsTable/TeamsPage.js'; +import { EUsersAdministrationSub, UsersAdministrationNavigationService } from './UsersAdministrationNavigationService.js'; +import { UsersPage } from './UsersTable/UsersPage.js'; +import { UsersAdministrationService } from './UsersAdministrationService.js'; -const tabsStyles = css` - tab-inner { - height: 100%; - } - - tab-outer { - height: 100%; - } -`; +const tabPanelRegistry: StyleRegistry = [[TabPanelStyles, { mode: 'append', styles: [tabPanelStyle] }]]; -const tabStyle = [ADMINISTRATION_TOOLS_PANEL_STYLES, BASE_TAB_STYLES, tabsStyles, UNDERLINE_TAB_STYLES]; +const mainTabsRegistry: StyleRegistry = [ + [TabStyles, { mode: 'append', styles: [tabStyle] }], + [TabTitleStyles, { mode: 'append', styles: [TabTitleModuleStyles] }], +]; export const UsersAdministration: AdministrationItemContentComponent = observer(function UsersAdministration({ sub, param }) { const translate = useTranslate(); const usersAdministrationNavigationService = useService(UsersAdministrationNavigationService); + const usersAdministrationService = useService(UsersAdministrationService); const subName = sub?.name || EUsersAdministrationSub.Users; - const styles = useS(style); + const styles = useS(style, tabStyle); function openSub({ tabId }: ITabData) { if (subName === tabId) { @@ -46,24 +56,34 @@ export const UsersAdministration: AdministrationItemContentComponent = observer( usersAdministrationNavigationService.navToSub(tabId as EUsersAdministrationSub, param || undefined); } - return styled(useStyles(tabStyle))( - - - - - {translate('authentication_administration_item_users')} - - - {translate('administration_teams_tab_title')} - - + return ( + + + + + + {translate('authentication_administration_item_users')} + + + {translate('administration_teams_tab_title')} + + + - - - - - - - , + + + + + + + + + + ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationNavigationService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationNavigationService.ts index 5790248cb0..8d7b70281b 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationNavigationService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationNavigationService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,15 +8,16 @@ import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { injectable } from '@cloudbeaver/core-di'; -import { ADMINISTRATION_ITEM_USER_CREATE_PARAM } from './ADMINISTRATION_ITEM_USER_CREATE_PARAM'; +import { ADMINISTRATION_ITEM_USER_CREATE_PARAM } from './ADMINISTRATION_ITEM_USER_CREATE_PARAM.js'; export enum EUsersAdministrationSub { Users = 'users', Teams = 'teams', MetaProperties = 'metaProperties', + Permissions = 'permissions', } -@injectable() +@injectable(() => [AdministrationScreenService]) export class UsersAdministrationNavigationService { static ItemName = 'users'; @@ -32,7 +33,7 @@ export class UsersAdministrationNavigationService { this.navToSub(EUsersAdministrationSub.Users, ADMINISTRATION_ITEM_USER_CREATE_PARAM); } - navToSub(sub: EUsersAdministrationSub, param?: string): void { + navToSub(sub: EUsersAdministrationSub | string, param?: string): void { this.administrationScreenService.navigateToItemSub(UsersAdministrationNavigationService.ItemName, sub, param); } } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts index 16eabe712a..9eb20cd2a3 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts @@ -1,33 +1,34 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 React from 'react'; -import { AdministrationItemService } from '@cloudbeaver/core-administration'; -import { AdminUser, TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; +import { AdministrationItemService, type IAdministrationItem } from '@cloudbeaver/core-administration'; +import { type AdminUser, TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { TabsContainer } from '@cloudbeaver/core-ui'; -import { CreateTeamService } from './Teams/CreateTeamService'; -import { EUsersAdministrationSub, UsersAdministrationNavigationService } from './UsersAdministrationNavigationService'; -import { CreateUserService } from './UsersTable/CreateUserService'; +import { CreateTeamService } from './Teams/TeamsTable/CreateTeamService.js'; +import { EUsersAdministrationSub, UsersAdministrationNavigationService } from './UsersAdministrationNavigationService.js'; +import { CreateUserService } from './UsersTable/CreateUserService.js'; const UserCredentialsList = React.lazy(async () => { - const { UserCredentialsList } = await import('./UsersTable/UserCredentialsList'); + const { UserCredentialsList } = await import('./UsersTable/UserCredentialsList.js'); return { default: UserCredentialsList }; }); const UsersDrawerItem = React.lazy(async () => { - const { UsersDrawerItem } = await import('./UsersDrawerItem'); + const { UsersDrawerItem } = await import('./UsersDrawerItem.js'); return { default: UsersDrawerItem }; }); const UsersAdministration = React.lazy(async () => { - const { UsersAdministration } = await import('./UsersAdministration'); + const { UsersAdministration } = await import('./UsersAdministration.js'); return { default: UsersAdministration }; }); @@ -35,9 +36,11 @@ export interface IUserDetailsInfoProps { user: AdminUser; } -@injectable() +@injectable(() => [AdministrationItemService, CreateUserService, TeamsResource, CreateTeamService, UsersResource]) export class UsersAdministrationService extends Bootstrap { - readonly userDetailsInfoPlaceholder = new PlaceholderContainer(); + readonly tabsContainer: TabsContainer; + readonly userDetailsInfoPlaceholder: PlaceholderContainer; + administrationItem!: IAdministrationItem; constructor( private readonly administrationItemService: AdministrationItemService, @@ -47,28 +50,26 @@ export class UsersAdministrationService extends Bootstrap { private readonly usersResource: UsersResource, ) { super(); + this.userDetailsInfoPlaceholder = new PlaceholderContainer(); + this.tabsContainer = new TabsContainer('Access Control'); } - register() { - this.administrationItemService.create({ + override register(): void { + this.administrationItem = this.administrationItemService.create({ name: UsersAdministrationNavigationService.ItemName, - order: 3, + order: 4, sub: [ { name: EUsersAdministrationSub.MetaProperties, }, { name: EUsersAdministrationSub.Users, - onDeActivate: this.cancelCreate.bind(this), + onDeActivate: this.cancelUserCreate.bind(this), }, { name: EUsersAdministrationSub.Teams, onActivate: this.loadTeams.bind(this), - onDeActivate: (param, configurationWizard, outside) => { - if (outside) { - this.teamsResource.cleanNewFlags(); - } - }, + onDeActivate: this.cancelTeamCreate.bind(this), }, ], defaultSub: EUsersAdministrationSub.Users, @@ -78,9 +79,7 @@ export class UsersAdministrationService extends Bootstrap { this.userDetailsInfoPlaceholder.add(UserCredentialsList, 0); } - load(): void | Promise {} - - private async cancelCreate(param: string | null, configurationWizard: boolean, outside: boolean) { + private cancelUserCreate(param: string | null, configurationWizard: boolean, outside: boolean) { if (param === 'create') { this.createUserService.close(); } @@ -90,7 +89,17 @@ export class UsersAdministrationService extends Bootstrap { } } - private async loadTeams(param: string | null) { + private cancelTeamCreate(param: string | null, configurationWizard: boolean, outside: boolean) { + if (param === 'create') { + this.createTeamService.dispose(); + } + + if (outside) { + this.teamsResource.cleanNewFlags(); + } + } + + private loadTeams(param: string | null) { if (param === 'create') { this.createTeamService.fillData(); } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersDrawerItem.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersDrawerItem.tsx index bef1632571..0e37b6f5bb 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersDrawerItem.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersDrawerItem.tsx @@ -1,23 +1,21 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import styled from 'reshadow'; - import type { AdministrationItemDrawerProps } from '@cloudbeaver/core-administration'; -import { Translate, useStyles } from '@cloudbeaver/core-blocks'; -import { Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; +import { Translate } from '@cloudbeaver/core-blocks'; +import { TabIcon, Tab, TabTitle } from '@cloudbeaver/core-ui'; -export const UsersDrawerItem: React.FC = function UsersDrawerItem({ item, onSelect, style, disabled }) { - return styled(useStyles(style))( +export const UsersDrawerItem: React.FC = function UsersDrawerItem({ item, onSelect, disabled }) { + return ( onSelect(item.name)}> - , + ); }; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUser.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUser.module.css new file mode 100644 index 0000000000..bc7ea90b2c --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUser.module.css @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.box { + height: 600px; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUser.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUser.tsx index 2742ef13f0..0f01bf5e0d 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUser.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUser.tsx @@ -1,62 +1,38 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import styled, { css } from 'reshadow'; +import { observer } from 'mobx-react-lite'; -import { Loader, Translate } from '@cloudbeaver/core-blocks'; +import { Container, Group, GroupTitle, Loader, s, Translate, useS, useTranslate } from '@cloudbeaver/core-blocks'; import type { IFormState } from '@cloudbeaver/core-ui'; -import { AdministrationUserForm } from '../UserForm/AdministrationUserForm'; -import type { IUserFormState, UserFormProps } from '../UserForm/AdministrationUserFormService'; - -const styles = css` - user-create { - display: flex; - flex-direction: column; - height: 600px; - overflow: hidden; - } - - title-bar { - composes: theme-border-color-background theme-typography--headline6 from global; - box-sizing: border-box; - padding: 16px 24px; - align-items: center; - display: flex; - font-weight: 400; - flex: auto 0 0; - } - - user-create-content { - composes: theme-background-secondary theme-text-on-secondary from global; - position: relative; - display: flex; - flex-direction: column; - flex: 1; - overflow: auto; - } -`; +import { AdministrationUserForm } from '../UserForm/AdministrationUserForm.js'; +import type { IUserFormState } from '../UserForm/AdministrationUserFormService.js'; +import style from './CreateUser.module.css'; interface Props { state: IFormState; onCancel: () => void; } -export const CreateUser: React.FC = function CreateUser({ state, onCancel }) { - return styled(styles)( - - +export const CreateUser = observer(function CreateUser({ state, onCancel }) { + const translate = useTranslate(); + const styles = useS(style); + + return ( + + - - + + - - , +
+ ); -}; +}); 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..4a4c67b570 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 @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -11,22 +11,24 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; import { ACTION_CREATE, ActionService, MenuService } from '@cloudbeaver/core-view'; -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'; +import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService.js'; +import { MENU_USERS_ADMINISTRATION } from '../../../Menus/MENU_USERS_ADMINISTRATION.js'; +import { ADMINISTRATION_ITEM_USER_CREATE_PARAM } from '../ADMINISTRATION_ITEM_USER_CREATE_PARAM.js'; +import { CreateUserService } from './CreateUserService.js'; -@injectable() +@injectable(() => [AuthProvidersResource, CreateUserService, MenuService, ActionService, AdministrationUsersManagementService]) export class CreateUserBootstrap extends Bootstrap { constructor( private readonly authProvidersResource: AuthProvidersResource, private readonly createUserService: CreateUserService, private readonly menuService: MenuService, private readonly actionService: ActionService, + private readonly administrationUsersManagementService: AdministrationUsersManagementService, ) { super(); } - register() { + override register() { this.menuService.addCreator({ menus: [MENU_USERS_ADMINISTRATION], getItems(context, items) { @@ -36,25 +38,28 @@ export class CreateUserBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'users-table-base', - isActionApplicable: (context, action) => { + menus: [MENU_USERS_ADMINISTRATION], + actions: [ACTION_CREATE], + isHidden: (context, action) => { if (action === ACTION_CREATE) { - return this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID); + return this.administrationUsersManagementService.externalUserProviderEnabled || !this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID); } return false; }, isDisabled: (context, action) => { if (action === ACTION_CREATE) { - const administrationItemRoute = context.tryGet(DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE); + const administrationItemRoute = context.get(DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE); return administrationItemRoute?.param === ADMINISTRATION_ITEM_USER_CREATE_PARAM && !!this.createUserService.state; } 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/CreateUserService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserService.ts index 204ccfe0ed..b7a601c703 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,23 +8,23 @@ import { makeObservable, observable } from 'mobx'; import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; -import { App, injectable } from '@cloudbeaver/core-di'; +import { injectable, IServiceProvider } from '@cloudbeaver/core-di'; -import { AdministrationUserFormService } from '../UserForm/AdministrationUserFormService'; -import { AdministrationUserFormState } from '../UserForm/AdministrationUserFormState'; -import { UsersAdministrationNavigationService } from '../UsersAdministrationNavigationService'; +import { AdministrationUserFormService } from '../UserForm/AdministrationUserFormService.js'; +import { AdministrationUserFormState } from '../UserForm/AdministrationUserFormState.js'; +import { UsersAdministrationNavigationService } from '../UsersAdministrationNavigationService.js'; export interface IToolsContainerProps { param: string | null | undefined; } -@injectable() +@injectable(() => [IServiceProvider, AdministrationUserFormService, UsersAdministrationNavigationService]) export class CreateUserService { state: AdministrationUserFormState | null; readonly toolsContainer: PlaceholderContainer; constructor( - private readonly app: App, + private readonly serviceProvider: IServiceProvider, private readonly administrationUserFormService: AdministrationUserFormService, private readonly usersAdministrationNavigationService: UsersAdministrationNavigationService, ) { @@ -50,11 +50,12 @@ export class CreateUserService { return; } - this.state = new AdministrationUserFormState(this.app, this.administrationUserFormService, { userId: null }); + this.state = new AdministrationUserFormState(this.serviceProvider, this.administrationUserFormService, { userId: null }); this.usersAdministrationNavigationService.navToCreate(); } clearUserTemplate(): void { + this.state?.dispose(); this.state = null; } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.module.css new file mode 100644 index 0000000000..220e53fea6 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.module.css @@ -0,0 +1,46 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.filterContainer { + display: flex; + gap: 12px; +} + +.filter { + flex: 1; +} + +.actions { + composes: theme-form-element-radius theme-background-surface theme-text-on-surface theme theme-border-color-background from global; + box-sizing: border-box; + border: 2px solid; + height: 32px; +} + +.button { + position: relative; + box-sizing: border-box; + background: inherit; + cursor: pointer; + width: 28px; + height: 100%; + padding: 4px; + + &:hover { + opacity: 0.8; + } +} + +.buttonActive { + background: var(--theme-secondary); +} + +.iconOrImage { + width: 100%; + height: 100%; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx index 3651742639..de91f23469 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx @@ -1,59 +1,18 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; import { useState } from 'react'; -import styled, { css, use } from 'reshadow'; -import { AuthRolesResource } from '@cloudbeaver/core-authentication'; -import { Combobox, Filter, Group, IconOrImage, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { Filter, Group, IconOrImage, Loader, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { IUserFilters, USER_ROLE_ALL, USER_STATUSES } from './useUsersTableFilters'; - -const styles = css` - filter-container { - display: flex; - gap: 12px; - } - - Filter { - flex: 1; - } - - actions { - composes: theme-form-element-radius theme-background-surface theme-text-on-surface theme theme-border-color-background from global; - box-sizing: border-box; - border: 2px solid; - height: 32px; - } - - button { - position: relative; - box-sizing: border-box; - background: inherit; - cursor: pointer; - width: 28px; - height: 100%; - padding: 4px; - - &:hover { - opacity: 0.8; - } - - &[|active] { - background: var(--theme-secondary); - } - } - - IconOrImage { - width: 100%; - height: 100%; - } -`; +import styles from './UsersTableFilters.module.css'; +import { UsersTableFiltersDetails } from './UsersTableFiltersDetails.js'; +import { type IUserFilters } from './useUsersTableFilters.js'; interface Props { filters: IUserFilters; @@ -61,46 +20,31 @@ interface Props { export const UsersTableFilters = observer(function UsersTableFilters({ filters }) { const translate = useTranslate(); - const style = useStyles(styles); - const authRolesResource = useResource(UsersTableFilters, AuthRolesResource, undefined); + const style = useS(styles); const [open, setOpen] = useState(false); - return styled(style)( + return ( - +
- - - - +
+
setOpen(!open)}> + +
+
+
{open && ( - - translate(value.label)} - keySelector={value => value.value} - keepSize - onSelect={filters.setStatus} - > - {translate('authentication_user_status')} - - {!!authRolesResource.data.length && ( - - {translate('authentication_user_role')} - - )} - + + + )} -
, + ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFiltersDetails.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFiltersDetails.tsx new file mode 100644 index 0000000000..1d8c37e588 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFiltersDetails.tsx @@ -0,0 +1,42 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { AuthRolesResource } from '@cloudbeaver/core-authentication'; +import { Select, useResource, useTranslate } from '@cloudbeaver/core-blocks'; + +import { type IUserFilters, USER_ROLE_ALL, USER_STATUSES } from './useUsersTableFilters.js'; + +interface Props { + filters: IUserFilters; +} + +export const UsersTableFiltersDetails = observer(function UsersTableFiltersDetails({ filters }) { + const translate = useTranslate(); + const authRolesResource = useResource(UsersTableFiltersDetails, AuthRolesResource, undefined); + + return ( +
+ + {!!authRolesResource.data.length && ( + + )} +
+ ); +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/useUsersTableFilters.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/useUsersTableFilters.tsx index ad542d901e..e9980af1b8 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/useUsersTableFilters.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/useUsersTableFilters.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.module.css new file mode 100644 index 0000000000..047b6ac807 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.module.css @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.gap { + gap: 16px; +} + +.overflow { + overflow: auto !important; +} 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..ee1ccd82e3 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 @@ -1,41 +1,34 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 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 { type AdminUser, UsersResource } from '@cloudbeaver/core-authentication'; +import { Checkbox, Link, Loader, Placeholder, TableColumnValue, TableItem, TableItemSelect, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import { clsx } from '@dbeaver/ui-kit'; -import { UsersAdministrationService } from '../UsersAdministrationService'; -import { UserEdit } from './UserEdit'; - -const styles = css` - TableColumnValue[expand] { - cursor: pointer; - } - TableColumnValue[|gap] { - gap: 16px; - } -`; +import { UsersAdministrationService } from '../UsersAdministrationService.js'; +import style from './User.module.css'; +import { UsersTableOptionsPanelService } from './UsersTableOptionsPanelService.js'; interface Props { user: AdminUser; displayAuthRole: boolean; + isManageable: boolean; selectable?: boolean; } -export const User = observer(function User({ user, displayAuthRole, selectable }) { +export const User = observer(function User({ user, displayAuthRole, isManageable, selectable }) { const usersAdministrationService = useService(UsersAdministrationService); - const teams = user.grantedTeams.join(', '); const usersService = useService(UsersResource); const notificationService = useService(NotificationService); + const usersTableOptionsPanelService = useService(UsersTableOptionsPanelService); const translate = useTranslate(); async function handleEnabledCheckboxChange(enabled: boolean) { @@ -50,40 +43,41 @@ export const User = observer(function User({ user, displayAuthRole, selec ? translate('administration_teams_team_granted_users_permission_denied') : undefined; - return styled(styles)( - + const teams = user.grantedTeams.join(', '); + + return ( + {selectable && ( )} - - - - - {user.userId} + usersTableOptionsPanelService.open(user.userId)}> + {user.userId} {displayAuthRole && ( - + {user.authRole} )} {teams} - - + +
+ +
- + -
, +
); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserCredentialsList.m.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserCredentialsList.m.css deleted file mode 100644 index 35e1d3024f..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserCredentialsList.m.css +++ /dev/null @@ -1,36 +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. - */ - -.container { - display: flex; - justify-content: center; - align-items: center; - width: 24px; - height: 46px; - cursor: pointer; -} - -.container:hover { - & .icon { - display: block; - } - - & .originIcon { - display: none; - } -} - -.icon { - display: none; - width: 16px; -} - -.staticImage { - width: 24px; - height: 24px; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserCredentialsList.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserCredentialsList.module.css new file mode 100644 index 0000000000..31c7765d46 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserCredentialsList.module.css @@ -0,0 +1,43 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.hasMoreIndicator { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 50%; + background-color: var(--theme-primary); + color: var(--theme-on-primary); + font-size: 12px; + font-weight: 700; + + &:hover { + opacity: 0.8; + } +} + +.staticImage, +.hasMoreIndicator { + width: 24px; + height: 24px; +} + +.menu { + flex-direction: row !important; +} + +.menuItem { + cursor: auto !important; +} + +.menuButton { + padding: 0; + background: transparent; + outline: none; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserCredentialsList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserCredentialsList.tsx index f6de878f22..98d4ec6939 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserCredentialsList.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserCredentialsList.tsx @@ -1,22 +1,22 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; import { Fragment } from 'react'; +import { Menu, MenuButton, MenuItem, useMenuState } from 'reakit'; -import { AUTH_PROVIDER_LOCAL_ID, UsersResource } from '@cloudbeaver/core-authentication'; -import { ConfirmationDialog, Icon, PlaceholderComponent, s, StaticImage, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; +import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; +import { BaseDropdownStyles, type PlaceholderComponent, s, StaticImage, useS, useTranslate } from '@cloudbeaver/core-blocks'; import type { ObjectOrigin } from '@cloudbeaver/core-sdk'; -import type { IUserDetailsInfoProps } from '../UsersAdministrationService'; -import style from './UserCredentialsList.m.css'; +import type { IUserDetailsInfoProps } from '../UsersAdministrationService.js'; +import style from './UserCredentialsList.module.css'; + +const MAX_VISIBLE_CREDENTIALS = 3; interface IUserCredentialsProps { origin: ObjectOrigin; @@ -35,46 +35,42 @@ export const UserCredentials = observer(function UserCred }); export const UserCredentialsList: PlaceholderComponent = observer(function UserCredentialsList({ user }) { + const styles = useS(style, BaseDropdownStyles); const translate = useTranslate(); - const usersResource = useService(UsersResource); - const commonDialogService = useService(CommonDialogService); - const notificationService = useService(NotificationService); - - const styles = useS(style); + const menu = useMenuState({ + placement: 'top', + gutter: 8, + }); - async function onDelete(originId: string, originName: string) { - const state = await commonDialogService.open(ConfirmationDialog, { - title: 'ui_data_delete_confirmation', - message: translate('authentication_administration_user_remove_credentials_confirmation_message', undefined, { - originName, - userId: user.userId, - }), - confirmActionText: 'ui_delete', - }); - - if (state !== DialogueStateResult.Rejected) { - try { - await usersResource.deleteCredentials(user.userId, originId); - notificationService.logSuccess({ title: 'authentication_administration_user_remove_credentials_success' }); - } catch (exception: any) { - notificationService.logException(exception, 'authentication_administration_user_remove_credentials_error'); - } - } - } + const visibleCredentials = user.origins.slice(0, MAX_VISIBLE_CREDENTIALS); return ( - {user.origins.map(origin => ( -
onDelete(origin.type, origin.displayName)} - > - - -
+ {visibleCredentials.map(origin => ( + ))} + + {user.origins.length > MAX_VISIBLE_CREDENTIALS && ( + <> + +
+ +{user.origins.length - MAX_VISIBLE_CREDENTIALS} +
+
+ + {user.origins.slice(MAX_VISIBLE_CREDENTIALS).map(origin => { + const isLocal = origin.type === AUTH_PROVIDER_LOCAL_ID; + const title = isLocal ? translate('authentication_administration_user_local') : origin.displayName; + + return ( + + + + ); + })} + + + )}
); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.m.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.m.css deleted file mode 100644 index 356875116b..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.m.css +++ /dev/null @@ -1,16 +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. - */ - -.box { - composes: theme-background-secondary theme-text-on-secondary from global; - box-sizing: border-box; - padding-bottom: 24px; - height: 600px; - display: flex; - flex-direction: column; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx index 26bc18db16..534df236f2 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx @@ -1,39 +1,70 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useState } from 'react'; -import { Loader, s, TableItemExpandProps, useS } from '@cloudbeaver/core-blocks'; -import { App, useService } from '@cloudbeaver/core-di'; +import { + ColoredContainer, + ConfirmationDialog, + GroupBack, + GroupTitle, + Loader, + type TableItemExpandProps, + Text, + useExecutor, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { FormMode } from '@cloudbeaver/core-ui'; -import { AdministrationUserForm } from '../UserForm/AdministrationUserForm'; -import { AdministrationUserFormService } from '../UserForm/AdministrationUserFormService'; -import { AdministrationUserFormState } from '../UserForm/AdministrationUserFormState'; -import style from './UserEdit.m.css'; +import { AdministrationUserForm } from '../UserForm/AdministrationUserForm.js'; +import { useAdministrationUserFormState } from './useAdministrationUserFormState.js'; +import { UsersTableOptionsPanelService } from './UsersTableOptionsPanelService.js'; export const UserEdit = observer>(function UserEdit({ item, onClose }) { - const styles = useS(style); - const administrationUserFormService = useService(AdministrationUserFormService); - const app = useService(App); - const [state] = useState(() => { - const state = new AdministrationUserFormState(app, administrationUserFormService, { - userId: item, - }); - state.setMode(FormMode.Edit); - return state; + const translate = useTranslate(); + const usersTableOptionsPanelService = useService(UsersTableOptionsPanelService); + const commonDialogService = useService(CommonDialogService); + const state = useAdministrationUserFormState(item, state => state.setMode(FormMode.Edit)); + + useExecutor({ + executor: usersTableOptionsPanelService.onClose, + handlers: [ + async function closeHandler(event, contexts) { + if (state.isChanged && event === 'before') { + const result = await commonDialogService.open(ConfirmationDialog, { + title: 'ui_save_reminder', + message: 'ui_are_you_sure', + confirmActionText: 'ui_yes', + }); + + if (result === DialogueStateResult.Rejected) { + ExecutorInterrupter.interrupt(contexts); + } + } + }, + ], }); return ( -
+ + + + + {translate('ui_edit')} + {state.state.userId ? ` "${state.state.userId}"` : ''} + + + -
+ ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationMenuBarItemStyles.m.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationMenuBarItemStyles.m.css deleted file mode 100644 index 84175204ff..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationMenuBarItemStyles.m.css +++ /dev/null @@ -1,9 +0,0 @@ -.menuBarItem { - & .menuBarItemLabel { - font-size: 14px; - } - - & .menuBarItemIcon + .menuBarItemLabel { - padding-left: initial; - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationMenuBarItemStyles.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationMenuBarItemStyles.module.css new file mode 100644 index 0000000000..9f71d71f92 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationMenuBarItemStyles.module.css @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.menuBarItemGroup { + & .menuBarItem { + & .menuBarItemBox { + gap: 0; + } + & .menuBarItemLabel { + font-size: 14px; + } + + & .menuBarItemIcon + .menuBarItemLabel { + padding-left: initial; + } + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.m.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.m.css deleted file mode 100644 index b669078cee..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.m.css +++ /dev/null @@ -1,8 +0,0 @@ -.toolsPanel { - border-bottom: none; - border-radius: inherit; - - & .menuBar { - height: initial; - } -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.module.css new file mode 100644 index 0000000000..07295898c4 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.module.css @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.toolsPanel .menuBar { + height: initial; +} 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..3ba6260980 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 @@ -1,19 +1,19 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { s, SContext, StyleRegistry, ToolsAction, ToolsPanel, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { s, SContext, type StyleRegistry, ToolsAction, ToolsPanel, useTranslate } from '@cloudbeaver/core-blocks'; import { MenuBar, MenuBarItemStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; -import { MENU_USERS_ADMINISTRATION } from '../../../Menus/MENU_USERS_ADMINISTRATION'; -import UsersAdministrationMenuBarItemStyles from './UsersAdministrationMenuBarItemStyles.m.css'; -import styles from './UsersAdministrationToolsPanel.m.css'; +import { MENU_USERS_ADMINISTRATION } from '../../../Menus/MENU_USERS_ADMINISTRATION.js'; +import UsersAdministrationMenuBarItemStyles from './UsersAdministrationMenuBarItemStyles.module.css'; +import styles from './UsersAdministrationToolsPanel.module.css'; interface Props { onUpdate: () => void; @@ -34,7 +34,7 @@ export const UsersAdministrationToolsPanel = observer(function UsersAdmin const menu = useMenu({ menu: MENU_USERS_ADMINISTRATION }); return ( - + 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..69f3b161ad 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 @@ -1,62 +1,60 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 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 } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { CreateUser } from './CreateUser'; -import { CreateUserService } from './CreateUserService'; -import { UsersTableFilters } from './Filters/UsersTableFilters'; -import { useUsersTableFilters } from './Filters/useUsersTableFilters'; -import { UsersAdministrationToolsPanel } from './UsersAdministrationToolsPanel'; -import { UsersTable } from './UsersTable'; -import { useUsersTable } from './useUsersTable'; +import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService.js'; +import { CreateUser } from './CreateUser.js'; +import { CreateUserService } from './CreateUserService.js'; +import { UsersTableFilters } from './Filters/UsersTableFilters.js'; +import { useUsersTableFilters } from './Filters/useUsersTableFilters.js'; +import { UsersAdministrationToolsPanel } from './UsersAdministrationToolsPanel.js'; +import { UsersTable } from './UsersTable.js'; +import { useUsersTable } from './useUsersTable.js'; interface Props { - sub?: IAdministrationItemSubItem; param?: string | null; } -export const UsersPage = observer(function UsersPage({ sub, param }) { - const style = useStyles(ADMINISTRATION_TOOLS_PANEL_STYLES); +export const UsersPage = observer(function UsersPage({ param }) { 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 isManageable = !administrationUsersManagementService.externalUserProviderEnabled; - return styled(style)( - - + return ( + + - - {create && createUserService.state && ( - - - + + + + + + {create && createUserService.state && isManageable && ( + )} - - - - (function UsersPage({ sub, param }) { displayAuthRole={displayAuthRole} loading={loading} hasMore={table.hasMore} + isManageable={isManageable} onLoadMore={table.loadMore} /> - , + ); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.module.css new file mode 100644 index 0000000000..b4bcb559d9 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.module.css @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.loader.hidden { + opacity: 0; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx index 1ff20acc9e..06e2c8966b 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,18 +9,22 @@ import { observer } from 'mobx-react-lite'; import { Button, + Flex, Loader, + s, Table, TableBody, TableColumnHeader, TableColumnValue, TableHeader, TableItem, + useS, useTranslate, } from '@cloudbeaver/core-blocks'; import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; -import { User } from './User'; +import { User } from './User.js'; +import classes from './UsersTable.module.css'; interface Props { users: AdminUserInfoFragment[]; @@ -29,6 +33,7 @@ interface Props { displayAuthRole: boolean; loading?: boolean; hasMore: boolean; + isManageable: boolean; onLoadMore?: () => void; } @@ -39,27 +44,31 @@ export const UsersTable = observer(function UsersTable({ displayAuthRole, loading, hasMore, + isManageable, onLoadMore, }) { const translate = useTranslate(); const keys = users.map(user => user.userId); const colSpan = displayAuthRole ? 6 : 5; + const styles = useS(classes); return ( - - + + + {translate('authentication_user_name')} + + - {translate('authentication_user_name')} {displayAuthRole && {translate('authentication_user_role')}} {translate('authentication_user_team')} - {translate('authentication_user_enabled')} - {translate('authentication_user_credentials')} + {translate('authentication_user_enabled')} + {translate('authentication_administration_user_auth_methods')} {users.map(user => ( - + ))} {(loading || users.length === 0) && ( @@ -71,7 +80,7 @@ export const UsersTable = observer(function UsersTable({ {hasMore && ( - diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTableOptionsPanel.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTableOptionsPanel.tsx new file mode 100644 index 0000000000..520f23faaa --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTableOptionsPanel.tsx @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; + +import { UserEdit } from './UserEdit.js'; +import { UsersTableOptionsPanelService } from './UsersTableOptionsPanelService.js'; + +export const UsersTableOptionsPanel = observer(function UsersTableOptionsPanel() { + const translate = useTranslate(); + const usersTableOptionsPanelService = useService(UsersTableOptionsPanelService); + + if (!usersTableOptionsPanelService.itemId) { + return {translate('ui_not_found')}; + } + + return ; +}); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTableOptionsPanelService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTableOptionsPanelService.ts new file mode 100644 index 0000000000..e9d753bb2c --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTableOptionsPanelService.ts @@ -0,0 +1,30 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { UsersResource } from '@cloudbeaver/core-authentication'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { injectable } from '@cloudbeaver/core-di'; +import { BaseOptionsPanelService, OptionsPanelService } from '@cloudbeaver/core-ui'; + +const UsersTableOptionsPanel = importLazyComponent(() => import('./UsersTableOptionsPanel.js').then(m => m.UsersTableOptionsPanel)); +const panelGetter = () => UsersTableOptionsPanel; + +@injectable(() => [OptionsPanelService, UsersResource]) +export class UsersTableOptionsPanelService extends BaseOptionsPanelService { + constructor( + optionsPanelService: OptionsPanelService, + private readonly usersResource: UsersResource, + ) { + super(optionsPanelService, panelGetter); + + this.usersResource.onItemDelete.addHandler(data => { + if (data === this.itemId) { + this.close(); + } + }); + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useAdministrationUserFormState.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useAdministrationUserFormState.ts new file mode 100644 index 0000000000..cfba509bf1 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useAdministrationUserFormState.ts @@ -0,0 +1,37 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { useEffect, useRef } from 'react'; + +import { IServiceProvider, useService } from '@cloudbeaver/core-di'; + +import { AdministrationUserFormService } from '../UserForm/AdministrationUserFormService.js'; +import { AdministrationUserFormState } from '../UserForm/AdministrationUserFormState.js'; + +export function useAdministrationUserFormState(id: string | null, configure?: (state: AdministrationUserFormState) => any) { + const service = useService(AdministrationUserFormService); + const serviceProvider = useService(IServiceProvider); + const ref = useRef(null); + + if (ref.current?.state.userId !== id) { + ref.current?.dispose(); + ref.current = new AdministrationUserFormState(serviceProvider, service, { + userId: id, + }); + configure?.(ref.current); + } + + useEffect( + () => () => { + ref.current?.dispose(); + }, + [], + ); + + return ref.current; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx index f5c26d0798..bb39dfba91 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx @@ -1,21 +1,22 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, computed, observable } from 'mobx'; -import { AdminUser, compareUsers, UsersResource, UsersResourceFilterKey, UsersResourceNewUsers } from '@cloudbeaver/core-authentication'; +import { type AdminUser, compareUsers, UsersResource, UsersResourceFilterKey } from '@cloudbeaver/core-authentication'; import { ConfirmationDialogDelete, TableState, useObservableRef, useOffsetPagination, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { resourceKeyList } from '@cloudbeaver/core-resource'; -import { ILoadableState, isArraysEqual, isDefined } from '@cloudbeaver/core-utils'; +import { type ILoadableState, isArraysEqual } from '@cloudbeaver/core-utils'; +import { isDefined } from '@dbeaver/js-helpers'; -import type { IUserFilters } from './Filters/useUsersTableFilters'; +import type { IUserFilters } from './Filters/useUsersTableFilters.js'; interface State { loading: boolean; @@ -31,10 +32,12 @@ interface State { export function useUsersTable(filters: IUserFilters) { const translate = useTranslate(); const usersResource = useService(UsersResource); + const searchFilter = filters.search.trim().toLowerCase(); + const enabledStateFilter = filters.status === 'true' ? true : filters.status === 'false' ? false : undefined; const pagination = useOffsetPagination(UsersResource, { - key: UsersResourceFilterKey(filters.search, filters.status === 'true' ? true : filters.status === 'false' ? false : undefined), + key: UsersResourceFilterKey(searchFilter, enabledStateFilter), }); - const usersLoader = useResource(useUsersTable, usersResource, pagination.key); + const usersLoader = useResource(useUsersTable, usersResource, pagination.currentPage); const notificationService = useService(NotificationService); const commonDialogService = useService(CommonDialogService); @@ -47,7 +50,10 @@ export function useUsersTable(filters: IUserFilters) { }, get users() { const users = Array.from( - new Set([...this.usersLoader.resource.get(UsersResourceNewUsers), ...usersLoader.tryGetData.filter(isDefined).sort(compareUsers)]), + new Set([ + ...this.usersLoader.resource.get(UsersResourceFilterKey(searchFilter, enabledStateFilter)), + ...usersResource.get(pagination.allPages).filter(isDefined).sort(compareUsers), + ]), ); return filters.filterUsers(users.filter(isDefined)); }, @@ -64,7 +70,7 @@ export function useUsersTable(filters: IUserFilters) { return; } - const deletionList = this.state.selectedList.filter(([_, value]) => value).map(([userId]) => userId); + const deletionList = this.state.selectedList.filter(([_, value]) => value).map(([userId]) => userId!); if (deletionList.length === 0) { return; } @@ -85,7 +91,7 @@ export function useUsersTable(filters: IUserFilters) { this.loading = true; try { - await this.usersLoader.resource.delete(resourceKeyList(deletionList)); + await this.usersLoader.resource.deleteUsers(resourceKeyList(deletionList)); this.state.unselect(); for (const id of deletionList) { diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/ENTITY_ID_VALIDATION.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/ENTITY_ID_VALIDATION.ts new file mode 100644 index 0000000000..6a2c8cd1d7 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/ENTITY_ID_VALIDATION.ts @@ -0,0 +1,9 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 const ENTITY_ID_VALIDATION = /^(?!\.)[^\\/:\\"'<>|?*]+$/u; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministration.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministration.module.css new file mode 100644 index 0000000000..2a166791f1 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministration.module.css @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +/* TODO: same styles in cloudbeaver/webapp/packages/plugin-product-information-administration/src/shared/ProductInfoPage.module.css */ +.tabList { + position: relative; + flex-shrink: 0; + align-items: center; + padding: 0 24px; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministrationTab.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministrationTab.module.css new file mode 100644 index 0000000000..19bbfde8de --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministrationTab.module.css @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +/* TODO: same styles cloudbeaver/webapp/packages/plugin-product-information-administration/src/shared/ProductInfoPage.module.css */ +.administrationTabs { + & .tab { + height: 100%; + font-size: 14px; + font-weight: 700; + padding: 0 4px; + } + + & .tabInner { + height: 100%; + } + + & .tabOuter { + height: 100%; + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministrationTabPanel.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministrationTabPanel.module.css new file mode 100644 index 0000000000..cbfdc00795 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministrationTabPanel.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +/* TODO: same styles cloudbeaver/webapp/packages/plugin-product-information-administration/src/shared/ProductInfoPageTabPanel.module.css */ +.tabPanel { + overflow: auto; +} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministrationTabTitle.module.css b/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministrationTabTitle.module.css new file mode 100644 index 0000000000..8508b2ff1e --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/shared/UsersAdministrationTabTitle.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +/* TODO: same styles cloudbeaver/webapp/packages/plugin-product-information-administration/src/shared/ProductInfoPageTabTitle.module.css */ +.tabTitle { + font-weight: 700; +} 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..f4a410b4e8 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/AdministrationUsersManagementService.ts @@ -0,0 +1,42 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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.js'; + +@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/AuthenticationLocaleService.ts b/webapp/packages/plugin-authentication-administration/src/AuthenticationLocaleService.ts index 742afeeb87..e4d441fa6e 100644 --- a/webapp/packages/plugin-authentication-administration/src/AuthenticationLocaleService.ts +++ b/webapp/packages/plugin-authentication-administration/src/AuthenticationLocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class AuthenticationLocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-authentication-administration/src/Menus/MENU_USERS_ADMINISTRATION.ts b/webapp/packages/plugin-authentication-administration/src/Menus/MENU_USERS_ADMINISTRATION.ts index df2686eef1..dd97bb0bfb 100644 --- a/webapp/packages/plugin-authentication-administration/src/Menus/MENU_USERS_ADMINISTRATION.ts +++ b/webapp/packages/plugin-authentication-administration/src/Menus/MENU_USERS_ADMINISTRATION.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const MENU_USERS_ADMINISTRATION = createMenu('users-administration', 'Users Administration'); +export const MENU_USERS_ADMINISTRATION = createMenu('users-administration', { label: 'Users Administration' }); diff --git a/webapp/packages/plugin-authentication-administration/src/PluginBootstrap.ts b/webapp/packages/plugin-authentication-administration/src/PluginBootstrap.ts index 2a982a341c..45067c7bf3 100644 --- a/webapp/packages/plugin-authentication-administration/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-authentication-administration/src/PluginBootstrap.ts @@ -1,39 +1,32 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 React from 'react'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ServerConfigurationAdministrationNavService, ServerConfigurationService } from '@cloudbeaver/plugin-administration'; import { AuthenticationService } from '@cloudbeaver/plugin-authentication'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; -import { AuthConfigurationsAdministrationNavService } from './Administration/IdentityProviders/AuthConfigurationsAdministrationNavService'; +const AuthenticationProviders = importLazyComponent(() => + import('./Administration/ServerConfiguration/AuthenticationProviders.js').then(m => m.AuthenticationProviders), +); -const AuthenticationProviders = React.lazy(async () => { - const { AuthenticationProviders } = await import('./Administration/ServerConfiguration/AuthenticationProviders'); - return { default: AuthenticationProviders }; -}); - -@injectable() +@injectable(() => [ServerConfigurationService, ServerConfigurationAdministrationNavService, AuthenticationService]) export class PluginBootstrap extends Bootstrap { constructor( private readonly serverConfigurationService: ServerConfigurationService, private readonly serverConfigurationAdministrationNavService: ServerConfigurationAdministrationNavService, - private readonly authConfigurationsAdministrationNavService: AuthConfigurationsAdministrationNavService, private readonly authenticationService: AuthenticationService, ) { super(); } - register(): void { + override register(): void { this.serverConfigurationService.configurationContainer.add(AuthenticationProviders, 0); this.authenticationService.setConfigureAuthProvider(() => this.serverConfigurationAdministrationNavService.navToSettings()); - this.authenticationService.setConfigureIdentityProvider(() => this.authConfigurationsAdministrationNavService.navToCreate()); } - - load(): void | Promise {} } 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..692e04c8dd --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/externalUserProviderStatusContext.ts @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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..dbeb32c0cf 100644 --- a/webapp/packages/plugin-authentication-administration/src/index.ts +++ b/webapp/packages/plugin-authentication-administration/src/index.ts @@ -1,19 +1,27 @@ -import { manifest } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { manifest } from './manifest.js'; export default manifest; -export * from './Administration/IdentityProviders/IdentityProvidersServiceLink'; -export * from './Administration/IdentityProviders/AuthConfigurationsAdministrationNavService'; -export * from './Administration/Users/UsersAdministrationNavigationService'; -export * from './Administration/Users/Teams/TeamFormService'; -export * from './Administration/Users/Teams/ITeamFormProps'; -export * from './Administration/Users/Teams/Contexts/teamContext'; -export * from './Administration/Users/UsersTable/CreateUserService'; -export * from './Administration/Users/UsersAdministrationService'; -export * from './Administration/Users/UserForm/AdministrationUserFormService'; -export * from './Administration/Users/UserForm/AdministrationUserFormState'; -export * from './Administration/Users/UserForm/Info/DATA_CONTEXT_USER_FORM_INFO_PART'; -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 './Administration/Users/UsersAdministrationNavigationService.js'; +export * from './Administration/Users/UsersTable/CreateUserService.js'; +export * from './Administration/Users/UsersAdministrationService.js'; +export * from './Administration/Users/UserForm/AdministrationUserFormService.js'; +export * from './Administration/Users/UserForm/AdministrationUserFormState.js'; +export * from './Administration/Users/UserForm/Info/getUserFormInfoPart.js'; +export * from './Administration/Users/UserForm/Info/UserFormInfoPart.js'; +export * from './Administration/Users/UserForm/Info/UserFormInfoPartService.js'; +export * from './Administration/Users/Teams/TeamsForm/TeamsAdministrationFormService.js'; +export * from './Menus/MENU_USERS_ADMINISTRATION.js'; +export * from './AdministrationUsersManagementService.js'; +export * from './externalUserProviderStatusContext.js'; +export * from './Administration/Users/Teams/TeamsForm/Options/getTeamOptionsFormPart.js'; +export * from './Administration/Users/UsersTable/UsersTableOptionsPanelService.js'; diff --git a/webapp/packages/plugin-authentication-administration/src/locales/en.ts b/webapp/packages/plugin-authentication-administration/src/locales/en.ts index e2860ee282..72f83690a9 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/en.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/en.ts @@ -1,4 +1,5 @@ export default [ + ['authentication_administration_pages_label', 'Pages'], ['authentication_administration_user_connections_user_add', 'User Creation'], ['authentication_administration_user_connections_user_new', 'New user'], ['authentication_administration_user_connections_access_load_fail', "User's granted connections loading failed"], @@ -12,7 +13,11 @@ export default [ ['authentication_administration_user_origin_empty', 'No available details'], ['authentication_administration_user_info', 'Info'], ['authentication_administration_user_local', 'Local user'], - ['authentication_administration_item', 'Access Management'], + ['authentication_administration_user_auth_method', 'Auth Method'], + ['authentication_administration_user_auth_methods', 'Auth Methods'], + ['authentication_administration_user_auth_methods_empty', 'No available auth methods'], + ['authentication_administration_user_auth_method_no_details', 'No details available'], + ['authentication_administration_item', 'Users and Teams'], ['authentication_administration_item_users', 'Users'], ['authentication_administration_item_metaParameters', 'Meta Parameters'], ['authentication_administration_tools_add_tooltip', 'Create new user'], @@ -31,12 +36,30 @@ export default [ ['authentication_administration_users_filters_status_disabled', 'DISABLED'], ['authentication_administration_users_filters_status_all', 'ALL'], ['authentication_administration_users_empty', 'There are no users'], + ['authentication_administration_users_delete_user', 'Delete user'], + ['authentication_administration_users_delete_user_fail', 'Failed to delete user'], + ['authentication_administration_users_delete_user_success', 'User deleted'], + ['authentication_administration_users_disable_user_fail', 'Failed to disable user'], + ['authentication_administration_users_disable_user_success', 'User disabled'], + [ + 'authentication_administration_users_delete_user_confirmation_input_description', + 'Please type in the username of the account to confirm its deletion.', + ], + ['authentication_administration_users_delete_user_confirmation_input_placeholder', 'Type username here...'], + [ + 'authentication_administration_users_delete_user_disable_info', + 'Are you sure you want to delete "{arg:username}"? If you just want to prevent access temporarily, you can choose to disable the account instead.', + ], + [ + 'authentication_administration_users_delete_user_info', + 'Deleting this account will permanently remove all associated user data from the system. Please confirm you want to proceed with deletion of "{arg:username}" user.', + ], - ['authentication_administration_user_remove_credentials_error', 'Failed to remove user credentials'], - ['authentication_administration_user_remove_credentials_success', 'User credentials were removed'], + ['authentication_administration_user_delete_credentials_error', 'Failed to remove user credentials'], + ['authentication_administration_user_delete_credentials_success', 'User credentials were removed'], [ - 'authentication_administration_user_remove_credentials_confirmation_message', - 'Are you sure you want to delete "{arg:originName}" credentials from "{arg:userId}"?', + 'authentication_administration_user_delete_credentials_confirmation_message', + 'Are you sure you want to delete "{arg:originName}" auth method from "{arg:userId}"?', ], ['administration_configuration_wizard_configuration_admin', 'Administrator Credentials'], @@ -56,7 +79,7 @@ export default [ ['administration_identity_providers_tab_title', 'Identity Providers'], ['administration_identity_providers_provider', 'Provider'], ['administration_identity_providers_provider_id', 'ID'], - ['administration_identity_providers_provider_configuration_name', 'Configuration name'], + ['administration_identity_providers_provider_configuration_name', 'Configuration Name'], ['administration_identity_providers_provider_configuration_disabled', 'Disabled'], ['administration_identity_providers_provider_configuration_description', 'Description'], ['administration_identity_providers_provider_configuration_icon_url', 'Icon URL'], @@ -66,6 +89,7 @@ export default [ ['administration_identity_providers_wizard_description', 'Add identity providers'], ['administration_identity_providers_configuration_add', 'Configuration creation'], ['administration_identity_providers_choose_provider_placeholder', 'Select provider...'], + ['administration_identity_providers_choose_provider_placeholder_empty', 'No available providers'], ['administration_identity_providers_add_tooltip', 'Add new configuration'], ['administration_identity_providers_refresh_tooltip', 'Refresh configuration list'], ['administration_identity_providers_delete_tooltip', 'Delete selected configurations'], @@ -78,9 +102,9 @@ export default [ ['administration_teams_tab_title', 'Teams'], ['administration_teams_tab_description', 'Team management'], - ['administration_teams_team_creation', 'Team creation'], + ['administration_teams_team_creation', 'Team Creation'], ['administration_teams_team_id', 'Team ID'], - ['administration_teams_team_name', 'Team name'], + ['administration_teams_team_name', 'Team Name'], ['administration_teams_team_description', 'Description'], ['administration_teams_team_permissions', 'Permissions'], ['administration_teams_team_create_error', 'Create team error'], @@ -107,4 +131,21 @@ export default [ ['administration_teams_team_granted_connections_tab_title', 'Connections'], ['administration_teams_team_granted_connections_search_placeholder', 'Search for connection name...'], ['administration_teams_team_granted_connections_empty', 'No available connections'], + + ['plugin_authentication_administration_user_team_default_readonly_tooltip', "Default team. Can't be revoked"], + ['plugin_authentication_administration_team_default_users_tooltip', 'Default team. Contains all users'], + ['plugin_authentication_administration_team_user_team_role_supervisor', 'Supervisor'], + ['plugin_authentication_administration_team_user_team_role_supervisor_description', 'Supervisors can view their team’s executed queries'], + + ['plugin_authentication_administration_team_form_edit_label', 'Team editing form'], + ['plugin_authentication_administration_user_form_edit_label', 'User editing form'], + + [ + 'plugin_authentication_administration_user_username_validation_error', + "User's name may not contain the following symbols / : \" \\ ' <> | ? * and can't start with a dot", + ], + [ + 'plugin_authentication_administration_team_id_validation_error', + "Team's ID may not contain the following symbols / : \" \\ ' <> | ? * and can't start with a dot", + ], ]; diff --git a/webapp/packages/plugin-authentication-administration/src/locales/fr.ts b/webapp/packages/plugin-authentication-administration/src/locales/fr.ts new file mode 100644 index 0000000000..1fd297d68b --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/locales/fr.ts @@ -0,0 +1,140 @@ +export default [ + ['authentication_administration_pages_label', 'Pages'], + ['authentication_administration_user_connections_user_add', "Création d'utilisateur"], + ['authentication_administration_user_connections_user_new', 'Nouvel utilisateur'], + ['authentication_administration_user_connections_access_load_fail', "Échec du chargement des connexions accordées à l'utilisateur"], + ['authentication_administration_user_connections_access_connections_load_fail', 'Échec du chargement des connexions'], + ['authentication_administration_user_connections_access', 'Accès aux connexions'], + ['authentication_administration_user_connections_access_granted_by', 'Accordé par'], + ['authentication_administration_user_connections_access_granted_team', 'Équipe :'], + ['authentication_administration_user_connections_access_granted_directly', 'Directement'], + ['authentication_administration_user_connections_access_granted_unmanaged', 'Non géré'], + ['authentication_administration_user_connections_empty', 'Aucune connexion disponible'], + ['authentication_administration_user_origin_empty', 'Aucun détail disponible'], + ['authentication_administration_user_info', 'Info'], + ['authentication_administration_user_local', 'Utilisateur local'], + ['authentication_administration_user_auth_method', "Méthode d'authentification"], + ['authentication_administration_user_auth_methods', "Méthodes d'authentification"], + ['authentication_administration_user_auth_methods_empty', "Aucune méthode d'authentification disponible"], + ['authentication_administration_user_auth_method_no_details', 'Aucun détail disponible'], + ['authentication_administration_item', 'Users and Teams'], + ['authentication_administration_item_users', 'Utilisateurs'], + ['authentication_administration_item_metaParameters', 'Paramètres Méta'], + ['authentication_administration_tools_add_tooltip', 'Créer un nouvel utilisateur'], + ['authentication_administration_tools_refresh_tooltip', 'Rafraîchir la liste des utilisateurs'], + ['authentication_administration_tools_delete_tooltip', 'Supprimer les utilisateurs sélectionnés'], + ['authentication_administration_tools_refresh_success', 'La liste des utilisateurs a été rafraîchie'], + ['authentication_administration_tools_refresh_fail', 'Échec de la mise à jour des utilisateurs'], + ['authentication_administration_user_delete_fail', "Erreur de suppression de l'utilisateur"], + ['authentication_administration_user_update_failed', "Erreur de sauvegarde de l'utilisateur"], + ['authentication_administration_user_updated', 'Utilisateur mis à jour'], + ['authentication_administration_user_created', 'Utilisateur créé avec succès'], + ['authentication_administration_user_create_failed', 'Erreur de création du nouvel utilisateur'], + ['authentication_administration_users_delete_confirmation', 'Vous allez supprimer ces utilisateurs : '], + ['authentication_administration_users_filters_search_placeholder', "Rechercher le nom de l'utilisateur..."], + ['authentication_administration_users_filters_status_enabled', 'ACTIVÉ'], + ['authentication_administration_users_filters_status_disabled', 'DÉSACTIVÉ'], + ['authentication_administration_users_filters_status_all', 'TOUS'], + ['authentication_administration_users_empty', "Il n'y a pas d'utilisateurs"], + ['authentication_administration_users_delete_user', "Supprimer l'utilisateur"], + ['authentication_administration_users_delete_user_fail', "Échec de la suppression de l'utilisateur"], + ['authentication_administration_users_delete_user_success', 'Utilisateur supprimé'], + ['authentication_administration_users_disable_user_fail', "Échec de la désactivation de l'utilisateur"], + ['authentication_administration_users_disable_user_success', 'Utilisateur désactivé'], + [ + 'authentication_administration_users_delete_user_confirmation_input_description', + "Veuillez saisir le nom d'utilisateur du compte pour confirmer sa suppression.", + ], + ['authentication_administration_users_delete_user_confirmation_input_placeholder', "Saisir le nom d'utilisateur ici..."], + [ + 'authentication_administration_users_delete_user_disable_info', + 'Êtes-vous sûr de vouloir supprimer "{arg:username}" ? Si vous voulez simplement empêcher temporairement l\'accès, vous pouvez choisir de désactiver le compte à la place.', + ], + [ + 'authentication_administration_users_delete_user_info', + 'La suppression de ce compte supprimera définitivement toutes les données utilisateur associées du système. Veuillez confirmer que vous souhaitez procéder à la suppression de l\'utilisateur "{arg:username}".', + ], + + ['authentication_administration_user_delete_credentials_error', "Échec de la suppression des identifiants de l'utilisateur"], + ['authentication_administration_user_delete_credentials_success', "Les identifiants de l'utilisateur ont été supprimés"], + [ + 'authentication_administration_user_delete_credentials_confirmation_message', + 'Êtes-vous sûr de vouloir supprimer la méthode d\'authentification "{arg:originName}" de "{arg:userId}" ?', + ], + + ['administration_configuration_wizard_configuration_admin', "Identifiants de l'administrateur"], + ['administration_configuration_wizard_configuration_admin_name', 'Identifiant'], + ['administration_configuration_wizard_configuration_admin_password', 'Mot de passe'], + ['administration_configuration_wizard_configuration_anonymous_access', "Autoriser l'accès anonyme"], + [ + 'administration_configuration_wizard_configuration_anonymous_access_description', + 'Permet de travailler avec CloudBeaver sans authentification utilisateur', + ], + ['administration_configuration_wizard_configuration_authentication_group', "Paramètres d'authentification"], + ['administration_configuration_wizard_configuration_services_group', 'Services'], + ['administration_configuration_wizard_configuration_services', 'Services'], + ['administration_configuration_wizard_configuration_authentication', "Activer l'authentification des utilisateurs"], + [ + 'administration_configuration_wizard_configuration_authentication_description', + "Permet aux utilisateurs de s'authentifier. Sinon, seul l'accès anonyme est activé", + ], + + ['administration_identity_providers_tab_title', "Fournisseurs d'identité"], + ['administration_identity_providers_provider', 'Fournisseur'], + ['administration_identity_providers_provider_id', 'ID'], + ['administration_identity_providers_provider_configuration_name', 'Nom de la configuration'], + ['administration_identity_providers_provider_configuration_disabled', 'Désactivé'], + ['administration_identity_providers_provider_configuration_description', 'Description'], + ['administration_identity_providers_provider_configuration_icon_url', "URL de l'icône"], + ['administration_identity_providers_provider_configuration_parameters', 'Paramètres'], + ['administration_identity_providers_provider_configuration_links', 'Liens'], + ['administration_identity_providers_provider_configuration_links_metadata', 'Télécharger le fichier de métadonnées'], + ['administration_identity_providers_wizard_description', "Ajouter des fournisseurs d'identité"], + ['administration_identity_providers_configuration_add', 'Création de configuration'], + ['administration_identity_providers_choose_provider_placeholder', 'Sélectionner un fournisseur...'], + ['administration_identity_providers_choose_provider_placeholder_empty', 'No available providers'], + ['administration_identity_providers_add_tooltip', 'Ajouter une nouvelle configuration'], + ['administration_identity_providers_refresh_tooltip', 'Rafraîchir la liste des configurations'], + ['administration_identity_providers_delete_tooltip', 'Supprimer les configurations sélectionnées'], + ['administration_identity_providers_delete_confirmation', 'Vous allez supprimer ces configurations : '], + ['administration_identity_providers_provider_save_error', 'Erreur de sauvegarde de la configuration'], + ['administration_identity_providers_provider_create_error', 'Erreur de création de la configuration'], + ['administration_identity_providers_configuration_list_update_success', 'La liste des configurations a été rafraîchie'], + ['administration_identity_providers_configuration_list_update_fail', 'Échec du rafraîchissement de la liste des configurations'], + ['administration_identity_providers_service_link', 'Modifier les configurations'], + + ['administration_teams_tab_title', 'Équipes'], + ['administration_teams_tab_description', 'Gestion des équipes'], + ['administration_teams_team_creation', "Création d'équipe"], + ['administration_teams_team_id', "ID de l'équipe"], + ['administration_teams_team_name', "Nom de l'équipe"], + ['administration_teams_team_description', 'Description'], + ['administration_teams_team_permissions', 'Permissions'], + ['administration_teams_team_create_error', "Erreur de création de l'équipe"], + ['administration_teams_team_save_error', "Erreur de sauvegarde de l'équipe"], + ['administration_teams_team_list_update_success', 'La liste des équipes a été rafraîchie'], + ['administration_teams_team_list_update_fail', 'Échec du rafraîchissement de la liste des équipes'], + ['administration_teams_team_info_created', 'Équipe créée'], + ['administration_teams_team_info_updated', 'Équipe mise à jour'], + ['administration_teams_team_info_id_invalid', "Le champ '{alias:administration_teams_team_id}' ne peut pas être vide"], + ['administration_teams_team_info_exists', "Une équipe avec l'ID '{arg:teamId}' existe déjà"], + ['administration_teams_add_tooltip', 'Ajouter une nouvelle équipe'], + ['administration_teams_refresh_tooltip', 'Rafraîchir la liste des équipes'], + ['administration_teams_delete_tooltip', 'Supprimer les équipes sélectionnées'], + ['administration_teams_delete_confirmation', 'Vous allez supprimer ces équipes : '], + [ + 'administration_teams_delete_confirmation_users_note', + "Notez que les utilisateurs perdront leur affiliation à l'équipe et toutes les permissions associées", + ], + ['plugin_authentication_administration_team_form_edit_label', 'Team editing form'], + ['plugin_authentication_administration_user_form_edit_label', 'User editing form'], + + [ + 'plugin_authentication_administration_user_username_validation_error', + "User's name may not contain the following symbols / : \" \\ ' <> | ? * and can't start with a dot", + ], + [ + 'plugin_authentication_administration_team_id_validation_error', + "Team's ID may not contain the following symbols / : \" \\ ' <> | ? * and can't start with a dot", + ], +]; diff --git a/webapp/packages/plugin-authentication-administration/src/locales/it.ts b/webapp/packages/plugin-authentication-administration/src/locales/it.ts index a3552aaa86..215cc9f80f 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/it.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/it.ts @@ -1,4 +1,5 @@ export default [ + ['authentication_administration_pages_label', 'Pagine'], ['authentication_administration_user_connections_user_add', 'Creazione di Utente'], ['authentication_administration_user_connections_user_new', 'Nuovo utente'], ['authentication_administration_user_connections_access_load_fail', "Errore in fase di caricamento delle connessioni autorizzate all'utente"], @@ -11,7 +12,11 @@ export default [ ['authentication_administration_user_origin_empty', 'Nessun dettaglio disponibile'], ['authentication_administration_user_info', 'Info'], ['authentication_administration_user_local', 'Local user'], - ['authentication_administration_item', 'Utenti'], + ['authentication_administration_user_auth_method', 'Auth Method'], + ['authentication_administration_user_auth_methods', 'Auth Methods'], + ['authentication_administration_user_auth_methods_empty', 'No available auth methods'], + ['authentication_administration_user_auth_method_no_details', 'No details available'], + ['authentication_administration_item', 'Users and Teams'], ['authentication_administration_tools_add_tooltip', 'Create new user'], ['authentication_administration_tools_refresh_tooltip', 'Aggiorna la lista utenti'], ['authentication_administration_tools_delete_tooltip', 'Elimina gli utenti selezionati'], @@ -27,12 +32,30 @@ export default [ ['authentication_administration_users_filters_status_disabled', 'DISABLED'], ['authentication_administration_users_filters_status_all', 'ALL'], ['authentication_administration_users_empty', 'There are no users'], + ['authentication_administration_users_delete_user', 'Delete user'], + ['authentication_administration_users_delete_user_fail', 'Failed to delete user'], + ['authentication_administration_users_delete_user_success', 'User deleted'], + ['authentication_administration_users_disable_user_fail', 'Failed to disable user'], + ['authentication_administration_users_disable_user_success', 'User disabled'], + [ + 'authentication_administration_users_delete_user_confirmation_input_description', + 'Please type in the username of the account to confirm its deletion.', + ], + ['authentication_administration_users_delete_user_confirmation_input_placeholder', 'Type username here...'], + [ + 'authentication_administration_users_delete_user_disable_info', + 'Are you sure you want to delete "{arg:username}"? If you just want to prevent access temporarily, you can choose to disable the account instead.', + ], + [ + 'authentication_administration_users_delete_user_info', + 'Deleting this account will permanently remove all associated user data from the system. Please confirm you want to proceed with deletion of "{arg:username}" user.', + ], - ['authentication_administration_user_remove_credentials_error', 'Failed to remove user credentials'], - ['authentication_administration_user_remove_credentials_success', 'User credentials were removed'], + ['authentication_administration_user_delete_credentials_error', 'Failed to remove user credentials'], + ['authentication_administration_user_delete_credentials_success', 'User credentials were removed'], [ - 'authentication_administration_user_remove_credentials_confirmation_message', - 'Are you sure you want to delete "{arg:originName}" credentials from "{arg:userId}"?', + 'authentication_administration_user_delete_credentials_confirmation_message', + 'Are you sure you want to delete "{arg:originName}" auth method from "{arg:userId}"?', ], ['administration_teams_team_info_created', 'Team created'], @@ -52,4 +75,24 @@ export default [ 'administration_configuration_wizard_configuration_authentication_description', "Permetti agli utenti di autenticarsi. In alternativa solo l'accesso anonimo sarà attivo", ], + + ['administration_identity_providers_provider_id', 'ID'], + ['administration_identity_providers_provider_configuration_description', 'Descrizione'], + + ['plugin_authentication_administration_user_team_default_readonly_tooltip', "Default team. Can't be revoked"], + ['plugin_authentication_administration_team_default_users_tooltip', 'Default team. Contains all users'], + ['plugin_authentication_administration_team_user_team_role_supervisor', 'Supervisor'], + ['plugin_authentication_administration_team_user_team_role_supervisor_description', 'Supervisors can view their team’s executed queries'], + + ['plugin_authentication_administration_team_form_edit_label', 'Team editing form'], + ['plugin_authentication_administration_user_form_edit_label', 'User editing form'], + + [ + 'plugin_authentication_administration_user_username_validation_error', + "User's name may not contain the following symbols / : \" \\ ' <> | ? * and can't start with a dot", + ], + [ + 'plugin_authentication_administration_team_id_validation_error', + "Team's ID may not contain the following symbols / : \" \\ ' <> | ? * and can't start with a dot", + ], ]; diff --git a/webapp/packages/plugin-authentication-administration/src/locales/ru.ts b/webapp/packages/plugin-authentication-administration/src/locales/ru.ts index abae12330d..ef63645e25 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/ru.ts @@ -1,4 +1,5 @@ export default [ + ['authentication_administration_pages_label', 'Страницы'], ['authentication_administration_user_connections_user_add', 'Создание пользователя'], ['authentication_administration_user_connections_user_new', 'Новый пользователь'], ['authentication_administration_user_connections_access_load_fail', 'Не удалось загрузить доступные пользователю коннекшены'], @@ -12,7 +13,11 @@ export default [ ['authentication_administration_user_origin_empty', 'Дополнительная информация не доступна'], ['authentication_administration_user_info', 'Общее'], ['authentication_administration_user_local', 'Локальный пользователь'], - ['authentication_administration_item', 'Управление доступом'], + ['authentication_administration_user_auth_method', 'Способ входа'], + ['authentication_administration_user_auth_methods', 'Способы входа'], + ['authentication_administration_user_auth_methods_empty', 'Нет доступных способов входа'], + ['authentication_administration_user_auth_method_no_details', 'Дополнительная информация не доступна'], + ['authentication_administration_item', 'Пользователи и команды'], ['authentication_administration_item_users', 'Пользователи'], ['authentication_administration_item_metaParameters', 'Свойства Пользователей'], ['authentication_administration_tools_add_tooltip', 'Создать нового пользователя'], @@ -31,12 +36,30 @@ export default [ ['authentication_administration_users_filters_status_disabled', 'ВЫКЛЮЧЕН'], ['authentication_administration_users_filters_status_all', 'ВСЕ'], ['authentication_administration_users_empty', 'Нет пользователей'], + ['authentication_administration_users_delete_user', 'Удалить пользователя'], + ['authentication_administration_users_delete_user_fail', 'Не удалось удалить пользователя'], + ['authentication_administration_users_delete_user_success', 'Пользователь удален'], + ['authentication_administration_users_disable_user_fail', 'Не удалось отключить пользователя'], + ['authentication_administration_users_disable_user_success', 'Пользователь отключен'], + [ + 'authentication_administration_users_delete_user_confirmation_input_description', + 'Пожалуйста, введите имя пользователя учетной записи для подтверждения ее удаления.', + ], + ['authentication_administration_users_delete_user_confirmation_input_placeholder', 'Введите имя пользователя...'], + [ + 'authentication_administration_users_delete_user_disable_info', + 'Вы уверены, что хотите удалить пользователя "{arg:username}"? Если вы хотите временно ограничить доступ, то вы можете отключить учетную запись.', + ], + [ + 'authentication_administration_users_delete_user_info', + 'Удаление этой учетной записи навсегда уберет все связанные с ней пользовательские данные из системы. Пожалуйста, подтвердите, что вы хотите произвести удаление пользователя "{arg:username}"', + ], - ['authentication_administration_user_remove_credentials_error', 'Не удалось удалить учетные данные пользователя'], - ['authentication_administration_user_remove_credentials_success', 'Учетные данные пользователя удалены'], + ['authentication_administration_user_delete_credentials_error', 'Не удалось удалить учетные данные пользователя'], + ['authentication_administration_user_delete_credentials_success', 'Учетные данные пользователя удалены'], [ - 'authentication_administration_user_remove_credentials_confirmation_message', - 'Вы уверены что хотите удалить "{arg:originName}" учетные данные у "{arg:userId}"?', + 'authentication_administration_user_delete_credentials_confirmation_message', + 'Вы уверены что хотите удалить "{arg:originName}" способ авторизации у "{arg:userId}"?', ], ['administration_configuration_wizard_configuration_admin', 'Учетные данные администратора'], @@ -68,6 +91,7 @@ export default [ ['administration_identity_providers_wizard_description', 'Добавьте провайдеры идентификации'], ['administration_identity_providers_configuration_add', 'Создание конфигурации'], ['administration_identity_providers_choose_provider_placeholder', 'Выберите провайдер...'], + ['administration_identity_providers_choose_provider_placeholder_empty', 'Нет доступных провайдеров'], ['administration_identity_providers_add_tooltip', 'Добавить новую конфигурацию'], ['administration_identity_providers_refresh_tooltip', 'Обновить список конфигураций'], ['administration_identity_providers_delete_tooltip', 'Удалить выбранные конфигурации'], @@ -80,7 +104,7 @@ export default [ ['administration_teams_tab_title', 'Команды'], ['administration_teams_tab_description', 'Управление командами'], - ['administration_teams_team_creation', 'Создание команды'], + ['administration_teams_team_creation', 'Создание Команды'], ['administration_teams_team_id', 'ID Команды'], ['administration_teams_team_name', 'Название команды'], ['administration_teams_team_description', 'Описание'], @@ -97,7 +121,10 @@ export default [ ['administration_teams_refresh_tooltip', 'Обновить список команд'], ['administration_teams_delete_tooltip', 'Удалить выбранные команды'], ['administration_teams_delete_confirmation', 'Вы собираетесь удалить следующие команды: '], - ['administration_teams_delete_confirmation_users_note', 'Обратите внимание, что пользователи потеряют принадлежность к команде и права полученные от этой команды'], + [ + 'administration_teams_delete_confirmation_users_note', + 'Обратите внимание, что пользователи потеряют принадлежность к команде и права полученные от этой команды', + ], ['administration_teams_team_granted_users_tab_title', 'Пользователи'], ['administration_teams_team_granted_users_search_placeholder', 'Поиск по ID пользователя...'], @@ -109,4 +136,24 @@ export default [ ['administration_teams_team_granted_connections_tab_title', 'Подключения'], ['administration_teams_team_granted_connections_search_placeholder', 'Поиск по названию подключения...'], ['administration_teams_team_granted_connections_empty', 'Нет доступных подключений'], + + ['plugin_authentication_administration_user_team_default_readonly_tooltip', 'Команда по умолчанию. Не может быть отозвана'], + ['plugin_authentication_administration_team_default_users_tooltip', 'Команда по умолчанию. Содержит всех пользователей'], + ['plugin_authentication_administration_team_user_team_role_supervisor', 'Супервайзер'], + [ + 'plugin_authentication_administration_team_user_team_role_supervisor_description', + 'Супервайзеры могут просматривать выполненные запросы своей команды', + ], + + ['plugin_authentication_administration_team_form_edit_label', 'Форма редактирования команды'], + ['plugin_authentication_administration_user_form_edit_label', 'Форма редактирования пользователя'], + + [ + 'plugin_authentication_administration_user_username_validation_error', + 'Имя пользователя не может содержать следующие символы / : " \\ \' <> | ? * и не может начинаться с точки', + ], + [ + 'plugin_authentication_administration_team_id_validation_error', + 'ID команды не может содержать следующие символы / : " \\ \' <> | ? * и не может начинаться с точки', + ], ]; diff --git a/webapp/packages/plugin-authentication-administration/src/locales/vi.ts b/webapp/packages/plugin-authentication-administration/src/locales/vi.ts new file mode 100644 index 0000000000..7281f652b5 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/locales/vi.ts @@ -0,0 +1,149 @@ +export default [ + ['authentication_administration_pages_label', 'Pages'], + ['authentication_administration_user_connections_user_add', 'Tạo Người dùng'], + ['authentication_administration_user_connections_user_new', 'Người dùng mới'], + ['authentication_administration_user_connections_access_load_fail', 'Tải kết nối được cấp cho người dùng thất bại'], + ['authentication_administration_user_connections_access_connections_load_fail', 'Tải kết nối thất bại'], + ['authentication_administration_user_connections_access', 'Quyền truy cập Kết nối'], + ['authentication_administration_user_connections_access_granted_by', 'Được cấp bởi'], + ['authentication_administration_user_connections_access_granted_team', 'Nhóm:'], + ['authentication_administration_user_connections_access_granted_directly', 'Trực tiếp'], + ['authentication_administration_user_connections_access_granted_unmanaged', 'Không được quản lý'], + ['authentication_administration_user_connections_empty', 'Không có kết nối nào khả dụng'], + ['authentication_administration_user_origin_empty', 'Không có chi tiết nào khả dụng'], + ['authentication_administration_user_info', 'Thông tin'], + ['authentication_administration_user_local', 'Người dùng cục bộ'], + ['authentication_administration_user_auth_method', 'Phương thức xác thực'], + ['authentication_administration_user_auth_methods', 'Các phương thức xác thực'], + ['authentication_administration_user_auth_methods_empty', 'Không có phương thức xác thực nào khả dụng'], + ['authentication_administration_user_auth_method_no_details', 'Không có chi tiết nào khả dụng'], + ['authentication_administration_item', 'Người dùng và Nhóm'], + ['authentication_administration_item_users', 'Người dùng'], + ['authentication_administration_item_metaParameters', 'Tham số Meta'], + ['authentication_administration_tools_add_tooltip', 'Tạo người dùng mới'], + ['authentication_administration_tools_refresh_tooltip', 'Làm mới danh sách người dùng'], + ['authentication_administration_tools_delete_tooltip', 'Xóa người dùng đã chọn'], + ['authentication_administration_tools_refresh_success', 'Danh sách người dùng đã được làm mới'], + ['authentication_administration_tools_refresh_fail', 'Cập nhật người dùng thất bại'], + ['authentication_administration_user_delete_fail', 'Lỗi xóa người dùng'], + ['authentication_administration_user_update_failed', 'Lỗi lưu người dùng'], + ['authentication_administration_user_updated', 'Người dùng đã được cập nhật'], + ['authentication_administration_user_created', 'Người dùng đã được tạo thành công'], + ['authentication_administration_user_create_failed', 'Lỗi tạo người dùng mới'], + ['authentication_administration_users_delete_confirmation', 'Bạn sắp xóa các người dùng sau: '], + ['authentication_administration_users_filters_search_placeholder', 'Tìm kiếm tên người dùng...'], + ['authentication_administration_users_filters_status_enabled', 'BẬT'], + ['authentication_administration_users_filters_status_disabled', 'TẮT'], + ['authentication_administration_users_filters_status_all', 'TẤT CẢ'], + ['authentication_administration_users_empty', 'Không có người dùng nào'], + ['authentication_administration_users_delete_user', 'Xóa người dùng'], + ['authentication_administration_users_delete_user_fail', 'Xóa người dùng thất bại'], + ['authentication_administration_users_delete_user_success', 'Người dùng đã được xóa'], + ['authentication_administration_users_disable_user_fail', 'Tắt người dùng thất bại'], + ['authentication_administration_users_disable_user_success', 'Người dùng đã được tắt'], + [ + 'authentication_administration_users_delete_user_confirmation_input_description', + 'Vui lòng nhập tên người dùng của tài khoản để xác nhận việc xóa.', + ], + ['authentication_administration_users_delete_user_confirmation_input_placeholder', 'Nhập tên người dùng tại đây...'], + [ + 'authentication_administration_users_delete_user_disable_info', + 'Bạn có chắc chắn muốn xóa "{arg:username}" không? Nếu bạn chỉ muốn tạm thời ngăn chặn truy cập, bạn có thể chọn tắt tài khoản thay vì xóa.', + ], + [ + 'authentication_administration_users_delete_user_info', + 'Xóa tài khoản này sẽ xóa vĩnh viễn tất cả dữ liệu liên quan đến người dùng khỏi hệ thống. Vui lòng xác nhận bạn muốn tiếp tục xóa người dùng "{arg:username}".', + ], + ['authentication_administration_user_delete_credentials_error', 'Xóa thông tin xác thực của người dùng thất bại'], + ['authentication_administration_user_delete_credentials_success', 'Thông tin xác thực của người dùng đã được xóa'], + [ + 'authentication_administration_user_delete_credentials_confirmation_message', + 'Bạn có chắc chắn muốn xóa phương thức xác thực "{arg:originName}" khỏi "{arg:userId}" không?', + ], + ['administration_configuration_wizard_configuration_admin', 'Thông tin xác thực Quản trị viên'], + ['administration_configuration_wizard_configuration_admin_name', 'Đăng nhập'], + ['administration_configuration_wizard_configuration_admin_password', 'Mật khẩu'], + ['administration_configuration_wizard_configuration_anonymous_access', 'Cho phép truy cập ẩn danh'], + [ + 'administration_configuration_wizard_configuration_anonymous_access_description', + 'Cho phép làm việc với CloudBeaver mà không cần xác thực người dùng', + ], + ['administration_configuration_wizard_configuration_authentication_group', 'Cài đặt xác thực'], + ['administration_configuration_wizard_configuration_services_group', 'Dịch vụ'], + ['administration_configuration_wizard_configuration_services', 'Dịch vụ'], + ['administration_configuration_wizard_configuration_authentication', 'Bật xác thực người dùng'], + [ + 'administration_configuration_wizard_configuration_authentication_description', + 'Cho phép người dùng xác thực. Nếu không, chỉ truy cập ẩn danh được bật', + ], + ['administration_identity_providers_tab_title', 'Nhà cung cấp Danh tính'], + ['administration_identity_providers_provider', 'Nhà cung cấp'], + ['administration_identity_providers_provider_id', 'ID'], + ['administration_identity_providers_provider_configuration_name', 'Tên Cấu hình'], + ['administration_identity_providers_provider_configuration_disabled', 'Đã tắt'], + ['administration_identity_providers_provider_configuration_description', 'Mô tả'], + ['administration_identity_providers_provider_configuration_icon_url', 'URL Biểu tượng'], + ['administration_identity_providers_provider_configuration_parameters', 'Tham số'], + ['administration_identity_providers_provider_configuration_links', 'Liên kết'], + ['administration_identity_providers_provider_configuration_links_metadata', 'Tải xuống tệp siêu dữ liệu'], + ['administration_identity_providers_wizard_description', 'Thêm nhà cung cấp danh tính'], + ['administration_identity_providers_configuration_add', 'Tạo cấu hình'], + ['administration_identity_providers_choose_provider_placeholder', 'Chọn nhà cung cấp...'], + ['administration_identity_providers_choose_provider_placeholder_empty', 'Không có nhà cung cấp nào khả dụng'], + ['administration_identity_providers_add_tooltip', 'Thêm cấu hình mới'], + ['administration_identity_providers_refresh_tooltip', 'Làm mới danh sách cấu hình'], + ['administration_identity_providers_delete_tooltip', 'Xóa các cấu hình đã chọn'], + ['administration_identity_providers_delete_confirmation', 'Bạn sắp xóa các cấu hình sau: '], + ['administration_identity_providers_provider_save_error', 'Lỗi lưu cấu hình'], + ['administration_identity_providers_provider_create_error', 'Lỗi tạo cấu hình'], + ['administration_identity_providers_configuration_list_update_success', 'Danh sách cấu hình đã được làm mới'], + ['administration_identity_providers_configuration_list_update_fail', 'Làm mới danh sách cấu hình thất bại'], + ['administration_identity_providers_service_link', 'Chỉnh sửa cấu hình'], + ['administration_teams_tab_title', 'Nhóm'], + ['administration_teams_tab_description', 'Quản lý nhóm'], + ['administration_teams_team_creation', 'Tạo Nhóm'], + ['administration_teams_team_id', 'ID Nhóm'], + ['administration_teams_team_name', 'Tên Nhóm'], + ['administration_teams_team_description', 'Mô tả'], + ['administration_teams_team_permissions', 'Quyền'], + ['administration_teams_team_create_error', 'Lỗi tạo nhóm'], + ['administration_teams_team_save_error', 'Lỗi lưu nhóm'], + ['administration_teams_team_list_update_success', 'Danh sách nhóm đã được làm mới'], + ['administration_teams_team_list_update_fail', 'Làm mới danh sách nhóm thất bại'], + ['administration_teams_team_info_created', 'Nhóm đã được tạo'], + ['administration_teams_team_info_updated', 'Nhóm đã được cập nhật'], + ['administration_teams_team_info_id_invalid', "Trường '{alias:administration_teams_team_id}' không được để trống"], + ['administration_teams_team_info_exists', "Nhóm với ID '{arg:teamId}' đã tồn tại"], + ['administration_teams_add_tooltip', 'Thêm nhóm mới'], + ['administration_teams_refresh_tooltip', 'Làm mới danh sách nhóm'], + ['administration_teams_delete_tooltip', 'Xóa các nhóm đã chọn'], + ['administration_teams_delete_confirmation', 'Bạn sắp xóa các nhóm sau: '], + ['administration_teams_delete_confirmation_users_note', 'Lưu ý rằng người dùng sẽ mất liên kết với nhóm và tất cả quyền liên quan'], + ['administration_teams_team_granted_users_tab_title', 'Người dùng'], + ['administration_teams_team_granted_users_search_placeholder', 'Tìm kiếm ID người dùng...'], + ['administration_teams_team_granted_users_user_id', 'ID Người dùng'], + ['administration_teams_team_granted_users_user_name', 'Tên Người dùng'], + ['administration_teams_team_granted_users_empty', 'Không có người dùng nào khả dụng'], + ['administration_teams_team_granted_users_permission_denied', 'Bạn không thể chỉnh sửa quyền của chính mình'], + ['administration_teams_team_granted_connections_tab_title', 'Kết nối'], + ['administration_teams_team_granted_connections_search_placeholder', 'Tìm kiếm tên kết nối...'], + ['administration_teams_team_granted_connections_empty', 'Không có kết nối nào khả dụng'], + ['plugin_authentication_administration_user_team_default_readonly_tooltip', 'Nhóm mặc định. Không thể thu hồi'], + ['plugin_authentication_administration_team_default_users_tooltip', 'Nhóm mặc định. Chứa tất cả người dùng'], + ['plugin_authentication_administration_team_user_team_role_supervisor', 'Giám sát'], + [ + 'plugin_authentication_administration_team_user_team_role_supervisor_description', + 'Giám sát viên có thể xem các truy vấn đã thực hiện của nhóm mình', + ], + ['plugin_authentication_administration_team_form_edit_label', 'Biểu mẫu chỉnh sửa nhóm'], + ['plugin_authentication_administration_user_form_edit_label', 'Biểu mẫu chỉnh sửa người dùng'], + + [ + 'plugin_authentication_administration_user_username_validation_error', + "User's name may not contain the following symbols / : \" \\ ' <> | ? * and can't start with a dot", + ], + [ + 'plugin_authentication_administration_team_id_validation_error', + "Team's ID may not contain the following symbols / : \" \\ ' <> | ? * and can't start with a dot", + ], +]; diff --git a/webapp/packages/plugin-authentication-administration/src/locales/zh.ts b/webapp/packages/plugin-authentication-administration/src/locales/zh.ts index 1698cda38b..bc8f83fcc8 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/zh.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/zh.ts @@ -1,4 +1,5 @@ export default [ + ['authentication_administration_pages_label', '页面'], ['authentication_administration_user_connections_user_add', '创建用户'], ['authentication_administration_user_connections_user_new', '新用户'], ['authentication_administration_user_connections_access_load_fail', '用户授予的连接加载失败'], @@ -10,33 +11,39 @@ export default [ ['authentication_administration_user_connections_empty', '没有可用连接'], ['authentication_administration_user_origin_empty', '没有可用详情'], ['authentication_administration_user_info', '信息'], - ['authentication_administration_user_local', 'Local user'], - ['authentication_administration_item', '访问管理'], + ['authentication_administration_user_auth_method', '认证方式'], + ['authentication_administration_user_auth_methods', '认证方式'], + ['authentication_administration_user_auth_methods_empty', '无可用认证方式'], + ['authentication_administration_user_auth_method_no_details', '无可用详情'], + ['authentication_administration_user_local', '本地用户'], + ['authentication_administration_item', '用户和群组'], ['authentication_administration_item_users', '用户'], ['authentication_administration_item_metaParameters', '元参数'], - ['authentication_administration_tools_add_tooltip', 'Create new user'], + ['authentication_administration_tools_add_tooltip', '添加新用户'], ['authentication_administration_tools_refresh_tooltip', '刷新用户列表'], ['authentication_administration_tools_delete_tooltip', '删除选中的用户'], ['authentication_administration_tools_refresh_success', '用户列表已刷新'], - ['authentication_administration_tools_refresh_fail', 'Users update failed'], - ['authentication_administration_user_delete_fail', 'Error deleting user'], + ['authentication_administration_tools_refresh_fail', '用户更新失败'], + ['authentication_administration_user_delete_fail', '删除用户出错'], ['authentication_administration_user_update_failed', '保存用户出错'], ['authentication_administration_user_updated', '用户信息已更新'], ['authentication_administration_user_created', '创建用户成功'], ['authentication_administration_user_create_failed', '创建新用户出错'], ['authentication_administration_users_delete_confirmation', '您将删除这些用户:'], - ['authentication_administration_users_filters_search_placeholder', 'Search for the user name...'], - ['authentication_administration_users_filters_status_enabled', 'ENABLED'], - ['authentication_administration_users_filters_status_disabled', 'DISABLED'], - ['authentication_administration_users_filters_status_all', 'ALL'], - ['authentication_administration_users_empty', 'There are no users'], + ['authentication_administration_users_filters_search_placeholder', '搜索用户名称...'], + ['authentication_administration_users_filters_status_enabled', '启用'], + ['authentication_administration_users_filters_status_disabled', '禁用'], + ['authentication_administration_users_filters_status_all', '全部'], + ['authentication_administration_users_empty', '无用户'], + ['authentication_administration_users_delete_user', '删除用户'], + ['authentication_administration_users_delete_user_fail', '删除用户失败'], + ['authentication_administration_users_delete_user_success', '用户已删除'], + ['authentication_administration_users_disable_user_fail', '删除用户失败'], + ['authentication_administration_users_disable_user_success', '用户已禁用'], - ['authentication_administration_user_remove_credentials_error', 'Failed to remove user credentials'], - ['authentication_administration_user_remove_credentials_success', 'User credentials were removed'], - [ - 'authentication_administration_user_remove_credentials_confirmation_message', - 'Are you sure you want to delete "{arg:originName}" credentials from "{arg:userId}"?', - ], + ['authentication_administration_user_delete_credentials_error', '删除用户凭证失败'], + ['authentication_administration_user_delete_credentials_success', '用户凭证已移除'], + ['authentication_administration_user_delete_credentials_confirmation_message', '确定要从“{arg:userId}”中删除“{arg:originName}”身份验证方法吗?'], ['administration_configuration_wizard_configuration_admin', '管理员凭据'], ['administration_configuration_wizard_configuration_admin_name', '登录'], @@ -48,6 +55,19 @@ export default [ ['administration_configuration_wizard_configuration_services', '服务'], ['administration_configuration_wizard_configuration_authentication', '启用用户认证'], ['administration_configuration_wizard_configuration_authentication_description', '允许用户进行身份验证。否则仅启用匿名访问'], + [ + 'authentication_administration_users_delete_user_confirmation_input_description', + 'Please type in the username of the account to confirm its deletion.', + ], + ['authentication_administration_users_delete_user_confirmation_input_placeholder', 'Type username here...'], + [ + 'authentication_administration_users_delete_user_disable_info', + 'Are you sure you want to delete "{arg:username}"? If you just want to prevent access temporarily, you can choose to disable the account instead.', + ], + [ + 'authentication_administration_users_delete_user_info', + 'Deleting this account will permanently remove all associated user data from the system. Please confirm you want to proceed with deletion of "{arg:username}" user.', + ], ['administration_identity_providers_tab_title', '身份提供者'], ['administration_identity_providers_provider', '提供者'], @@ -62,6 +82,7 @@ export default [ ['administration_identity_providers_wizard_description', '添加身份提供者'], ['administration_identity_providers_configuration_add', '创建配置'], ['administration_identity_providers_choose_provider_placeholder', '选择提供者...'], + ['administration_identity_providers_choose_provider_placeholder_empty', 'No available providers'], ['administration_identity_providers_add_tooltip', '添加新配置'], ['administration_identity_providers_refresh_tooltip', '刷新配置列表'], ['administration_identity_providers_delete_tooltip', '删除选中配置'], @@ -89,4 +110,21 @@ export default [ ['administration_teams_team_granted_connections_tab_title', '连接'], ['administration_teams_team_granted_connections_search_placeholder', '搜索连接名称...'], ['administration_teams_team_granted_connections_empty', '没有可用连接'], + + ['plugin_authentication_administration_user_team_default_readonly_tooltip', "Default team. Can't be revoked"], + ['plugin_authentication_administration_team_default_users_tooltip', 'Default team. Contains all users'], + ['plugin_authentication_administration_team_user_team_role_supervisor', 'Supervisor'], + ['plugin_authentication_administration_team_user_team_role_supervisor_description', 'Supervisors can view their team’s executed queries'], + + ['plugin_authentication_administration_team_form_edit_label', 'Team editing form'], + ['plugin_authentication_administration_user_form_edit_label', 'User editing form'], + + [ + 'plugin_authentication_administration_user_username_validation_error', + "User's name may not contain the following symbols / : \" \\ ' <> | ? * and can't start with a dot", + ], + [ + 'plugin_authentication_administration_team_id_validation_error', + "Team's ID may not contain the following symbols / : \" \\ ' <> | ? * and can't start with a dot", + ], ]; diff --git a/webapp/packages/plugin-authentication-administration/src/manifest.ts b/webapp/packages/plugin-authentication-administration/src/manifest.ts index 1bd7799b4c..1f53c4fab1 100644 --- a/webapp/packages/plugin-authentication-administration/src/manifest.ts +++ b/webapp/packages/plugin-authentication-administration/src/manifest.ts @@ -1,68 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { AuthConfigurationFormService } from './Administration/IdentityProviders/AuthConfigurationFormService'; -import { AuthConfigurationsAdministrationNavService } from './Administration/IdentityProviders/AuthConfigurationsAdministrationNavService'; -import { AuthConfigurationsAdministrationService } from './Administration/IdentityProviders/AuthConfigurationsAdministrationService'; -import { CreateAuthConfigurationService } from './Administration/IdentityProviders/CreateAuthConfigurationService'; -import { AuthConfigurationOptionsTabService } from './Administration/IdentityProviders/Options/AuthConfigurationOptionsTabService'; -import { ServerConfigurationAuthenticationBootstrap } from './Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap'; -import { CreateTeamService } from './Administration/Users/Teams/CreateTeamService'; -import { GrantedConnectionsTabService } from './Administration/Users/Teams/GrantedConnections/GrantedConnectionsTabService'; -import { GrantedUsersTabService } from './Administration/Users/Teams/GrantedUsers/GrantedUsersTabService'; -import { TeamOptionsTabService } from './Administration/Users/Teams/Options/TeamOptionsTabService'; -import { TeamFormService } from './Administration/Users/Teams/TeamFormService'; -import { TeamsAdministrationNavService } from './Administration/Users/Teams/TeamsAdministrationNavService'; -import { TeamsAdministrationService } from './Administration/Users/Teams/TeamsAdministrationService'; -import { AdministrationUserFormService } from './Administration/Users/UserForm/AdministrationUserFormService'; -import { UserFormConnectionAccessPartBootstrap } from './Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPartBootstrap'; -import { UserFormInfoPartBootstrap } from './Administration/Users/UserForm/Info/UserFormInfoPartBootstrap'; -import { UserFormInfoPartService } from './Administration/Users/UserForm/Info/UserFormInfoPartService'; -import { UserFormOriginPartBootstrap } from './Administration/Users/UserForm/Origin/UserFormOriginPartBootstrap'; -import { UserFormBaseBootstrap } from './Administration/Users/UserForm/UserFormBaseBootstrap'; -import { UsersAdministrationNavigationService } from './Administration/Users/UsersAdministrationNavigationService'; -import { UsersAdministrationService } from './Administration/Users/UsersAdministrationService'; -import { CreateUserBootstrap } from './Administration/Users/UsersTable/CreateUserBootstrap'; -import { CreateUserService } from './Administration/Users/UsersTable/CreateUserService'; -import { AuthenticationLocaleService } from './AuthenticationLocaleService'; -import { PluginBootstrap } from './PluginBootstrap'; - export const manifest: PluginManifest = { info: { name: 'Authentication Administration', }, - - providers: [ - PluginBootstrap, - UsersAdministrationService, - AuthenticationLocaleService, - CreateUserService, - UsersAdministrationNavigationService, - ServerConfigurationAuthenticationBootstrap, - AdministrationUserFormService, - AuthConfigurationsAdministrationService, - CreateAuthConfigurationService, - AuthConfigurationsAdministrationNavService, - AuthConfigurationFormService, - AuthConfigurationOptionsTabService, - TeamsAdministrationService, - CreateTeamService, - TeamsAdministrationNavService, - TeamFormService, - TeamOptionsTabService, - GrantedUsersTabService, - GrantedConnectionsTabService, - CreateUserBootstrap, - UserFormBaseBootstrap, - UserFormInfoPartBootstrap, - UserFormOriginPartBootstrap, - UserFormConnectionAccessPartBootstrap, - UserFormInfoPartService, - ], }; diff --git a/webapp/packages/plugin-authentication-administration/src/module.ts b/webapp/packages/plugin-authentication-administration/src/module.ts new file mode 100644 index 0000000000..74567e03af --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/module.ts @@ -0,0 +1,62 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { PluginBootstrap } from './PluginBootstrap.js'; +import { AuthenticationLocaleService } from './AuthenticationLocaleService.js'; +import { UsersTableOptionsPanelService } from './Administration/Users/UsersTable/UsersTableOptionsPanelService.js'; +import { CreateUserService } from './Administration/Users/UsersTable/CreateUserService.js'; +import { AdministrationUsersManagementService } from './AdministrationUsersManagementService.js'; +import { CreateUserBootstrap } from './Administration/Users/UsersTable/CreateUserBootstrap.js'; +import { UsersAdministrationService } from './Administration/Users/UsersAdministrationService.js'; +import { UserFormBaseBootstrap } from './Administration/Users/UserForm/UserFormBaseBootstrap.js'; +import { UsersAdministrationNavigationService } from './Administration/Users/UsersAdministrationNavigationService.js'; +import { UserFormOriginPartBootstrap } from './Administration/Users/UserForm/Origin/UserFormOriginPartBootstrap.js'; +import { UserFormInfoPartService } from './Administration/Users/UserForm/Info/UserFormInfoPartService.js'; +import { UserFormInfoPartBootstrap } from './Administration/Users/UserForm/Info/UserFormInfoPartBootstrap.js'; +import { UserFormConnectionAccessPartBootstrap } from './Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPartBootstrap.js'; +import { TeamsTableOptionsPanelService } from './Administration/Users/Teams/TeamsTable/TeamsTableOptionsPanelService.js'; +import { AdministrationUserFormService } from './Administration/Users/UserForm/AdministrationUserFormService.js'; +import { CreateTeamService } from './Administration/Users/Teams/TeamsTable/CreateTeamService.js'; +import { TeamsAdministrationFormService } from './Administration/Users/Teams/TeamsForm/TeamsAdministrationFormService.js'; +import { TeamOptionsTabService } from './Administration/Users/Teams/TeamsForm/Options/TeamOptionsTabService.js'; +import { GrantedUsersTabService } from './Administration/Users/Teams/TeamsForm/GrantedUsers/GrantedUsersTabService.js'; +import { GrantedConnectionsTabService } from './Administration/Users/Teams/TeamsForm/GrantedConnections/GrantedConnectionsTabService.js'; +import { TeamsAdministrationNavService } from './Administration/Users/Teams/TeamsAdministrationNavService.js'; +import { TeamsAdministrationService } from './Administration/Users/Teams/TeamsAdministrationService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-authentication-administration', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, PluginBootstrap) + .addSingleton(Bootstrap, CreateUserBootstrap) + .addSingleton(Bootstrap, UserFormBaseBootstrap) + .addSingleton(Bootstrap, UserFormOriginPartBootstrap) + .addSingleton(Bootstrap, UserFormInfoPartBootstrap) + .addSingleton(Bootstrap, UserFormConnectionAccessPartBootstrap) + .addSingleton(Bootstrap, proxy(UsersAdministrationService)) + .addSingleton(Bootstrap, TeamOptionsTabService) + .addSingleton(Bootstrap, AuthenticationLocaleService) + .addSingleton(Bootstrap, GrantedUsersTabService) + .addSingleton(Bootstrap, GrantedConnectionsTabService) + .addSingleton(TeamsAdministrationService) + .addSingleton(UsersTableOptionsPanelService) + .addSingleton(CreateUserService) + .addSingleton(AdministrationUsersManagementService) + .addSingleton(UsersAdministrationService) + .addSingleton(UsersAdministrationNavigationService) + .addSingleton(UserFormInfoPartService) + .addSingleton(TeamsTableOptionsPanelService) + .addSingleton(AdministrationUserFormService) + .addSingleton(CreateTeamService) + .addSingleton(TeamsAdministrationFormService) + .addSingleton(TeamsAdministrationNavService); + }, +}); diff --git a/webapp/packages/plugin-authentication-administration/tsconfig.json b/webapp/packages/plugin-authentication-administration/tsconfig.json index 79c5991084..3eaaba3abe 100644 --- a/webapp/packages/plugin-authentication-administration/tsconfig.json +++ b/webapp/packages/plugin-authentication-administration/tsconfig.json @@ -1,67 +1,77 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-administration/tsconfig.json" + "path": "../../common-react/@dbeaver/ui-kit" }, { - "path": "../plugin-authentication/tsconfig.json" + "path": "../../common-typescript/@dbeaver/js-helpers" }, { - "path": "../core-administration/tsconfig.json" + "path": "../core-administration" }, { - "path": "../core-authentication/tsconfig.json" + "path": "../core-authentication" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-data-context/tsconfig.json" + "path": "../core-connections" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-executor/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-executor" }, { - "path": "../core-projects/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-projects" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-sdk" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-view/tsconfig.json" + "path": "../core-utils" + }, + { + "path": "../core-view" + }, + { + "path": "../plugin-administration" + }, + { + "path": "../plugin-authentication" } ], "include": [ @@ -73,7 +83,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-authentication/package.json b/webapp/packages/plugin-authentication/package.json index a68484d5ea..f89f7e02b7 100644 --- a/webapp/packages/plugin-authentication/package.json +++ b/webapp/packages/plugin-authentication/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-authentication", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,38 +11,43 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-administration": "~0.1.0", - "@cloudbeaver/plugin-settings-menu": "~0.1.0", - "@cloudbeaver/core-administration": "~0.1.0", - "@cloudbeaver/core-authentication": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-routing": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-administration": "workspace:*", + "@cloudbeaver/core-authentication": "workspace:*", + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-routing": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-settings-menu": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-authentication/src/AuthenticationLocaleService.ts b/webapp/packages/plugin-authentication/src/AuthenticationLocaleService.ts index 742afeeb87..e4d441fa6e 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationLocaleService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationLocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class AuthenticationLocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index 20ab3ebac6..5120c5a207 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -11,30 +11,44 @@ import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { AppAuthService, AUTH_PROVIDER_LOCAL_ID, - AuthInfoService, AuthProviderContext, AuthProviderService, AuthProvidersResource, - IUserAuthConfiguration, - RequestedProvider, + type RequestedProvider, UserInfoResource, + type UserLogoutInfo, } from '@cloudbeaver/core-authentication'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import type { DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { Executor, ExecutorInterrupter, IExecutionContextProvider, IExecutorHandler } from '@cloudbeaver/core-executor'; +import { Executor, ExecutorInterrupter, type IExecutionContextProvider, type IExecutorHandler } from '@cloudbeaver/core-executor'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { ISessionAction, ServerConfigResource, sessionActionContext, SessionActionService, SessionDataResource } from '@cloudbeaver/core-root'; +import { type ISessionAction, ServerConfigResource, sessionActionContext, SessionActionService, SessionDataResource } from '@cloudbeaver/core-root'; import { ScreenService, WindowsService } from '@cloudbeaver/core-routing'; import { NavigationService } from '@cloudbeaver/core-ui'; +import { uuid } from '@cloudbeaver/core-utils'; -import { AuthDialogService } from './Dialog/AuthDialogService'; -import type { IAuthOptions } from './IAuthOptions'; -import { isAutoLoginSessionAction } from './isAutoLoginSessionAction'; +import { AuthDialogService } from './Dialog/AuthDialogService.js'; +import type { IAuthOptions } from './IAuthOptions.js'; +import { isAutoLoginSessionAction } from './isAutoLoginSessionAction.js'; -type AuthEventType = 'before' | 'after'; +export type AuthEventType = 'before' | 'after'; -@injectable() +@injectable(() => [ + ScreenService, + AppAuthService, + AuthDialogService, + UserInfoResource, + NotificationService, + AdministrationScreenService, + AuthProviderService, + AuthProvidersResource, + SessionDataResource, + ServerConfigResource, + WindowsService, + SessionActionService, + NavigationService, +]) export class AuthenticationService extends Bootstrap { readonly onLogout: Executor; readonly onLogin: Executor; @@ -54,7 +68,6 @@ export class AuthenticationService extends Bootstrap { private readonly authProviderService: AuthProviderService, private readonly authProvidersResource: AuthProvidersResource, private readonly sessionDataResource: SessionDataResource, - private readonly authInfoService: AuthInfoService, private readonly serverConfigResource: ServerConfigResource, private readonly windowsService: WindowsService, private readonly sessionActionService: SessionActionService, @@ -66,6 +79,7 @@ export class AuthenticationService extends Bootstrap { this.onLogin = new Executor(); this.onLogout.before(this.navigationService.navigationTask); + this.onLogin.before(this.navigationService.navigationTask, undefined, () => userInfoResource.isAnonymous()); this.authPromise = null; this.configureAuthProvider = null; @@ -91,22 +105,10 @@ export class AuthenticationService extends Bootstrap { return; } - let userAuthConfiguration: IUserAuthConfiguration | undefined = undefined; - - if (providerId) { - userAuthConfiguration = this.authInfoService.userAuthConfigurations.find( - c => c.providerId === providerId && c.configuration.id === configurationId, - ); - } else if (this.authInfoService.userAuthConfigurations.length > 0) { - userAuthConfiguration = this.authInfoService.userAuthConfigurations[0]; - } - - if (userAuthConfiguration?.configuration.signOutLink) { - this.logoutConfiguration(userAuthConfiguration); - } - try { - await this.userInfoResource.logout(providerId, configurationId); + const logoutResult = await this.userInfoResource.logout(providerId, configurationId); + + this.handleRedirectLinks(logoutResult.result); if (!this.administrationScreenService.isConfigurationMode && !providerId) { this.screenService.navigateToRoot(); @@ -114,15 +116,20 @@ export class AuthenticationService extends Bootstrap { await this.onLogout.execute('after'); } catch (exception: any) { - this.notificationService.logException(exception, "Can't logout"); + this.notificationService.logException(exception, 'authentication_logout_error'); } } - private async logoutConfiguration(configuration: IUserAuthConfiguration): Promise { - if (configuration.configuration.signOutLink) { - const id = `${configuration.configuration.id}-sign-out`; + // TODO handle all redirect links once we know what to do with multiple popups issue + private handleRedirectLinks(userLogoutInfo: UserLogoutInfo) { + const redirectLinks = userLogoutInfo.redirectLinks; + + if (redirectLinks.length) { + const url = redirectLinks[0]; + const id = `okta-logout-id-${uuid()}`; + const popup = this.windowsService.open(id, { - url: configuration.configuration.signOutLink, + url, target: id, width: 600, height: 700, @@ -149,16 +156,6 @@ export class AuthenticationService extends Bootstrap { options = observable(options); - this.authPromise = this.authDialogService - .showLoginForm(persistent, options) - .then(async state => { - await this.onLogin.execute('after'); - return state; - }) - .finally(() => { - this.authPromise = null; - }); - if (this.serverConfigResource.redirectOnFederatedAuth) { await this.authProvidersResource.load(CachedMapAllKey); @@ -168,7 +165,7 @@ export class AuthenticationService extends Bootstrap { const configurableProvider = providers.find(provider => provider.configurable); if (configurableProvider?.configurations?.length === 1) { - const configuration = configurableProvider.configurations[0]; + const configuration = configurableProvider.configurations[0]!; options.providerId = configurableProvider.id; options.configurationId = configuration.id; @@ -176,6 +173,24 @@ export class AuthenticationService extends Bootstrap { } } + if (this.authPromise) { + await this.waitAuth(); + return; + } + + this.authPromise = this.authDialogService + .showLoginForm(persistent, options) + .then(async state => { + if (state === DialogueStateResult.Rejected) { + return state; + } + await this.onLogin.execute('after'); + return state; + }) + .finally(() => { + this.authPromise = null; + }); + await this.authPromise; } @@ -190,7 +205,7 @@ export class AuthenticationService extends Bootstrap { await this.auth(true, { accessRequest: true, providerId: null, linkUser: false }); } - register(): void { + override register(): void { // this.sessionDataResource.beforeLoad.addHandler( // ExecutorInterrupter.interrupter(() => this.appAuthService.isAuthNeeded()) // ); @@ -204,8 +219,8 @@ export class AuthenticationService extends Bootstrap { this.administrationScreenService.ensurePermissions.addHandler(async () => { await this.waitAuth(); - const userInfo = await this.userInfoResource.load(); - if (userInfo) { + await this.userInfoResource.load(); + if (this.userInfoResource.isAuthenticated()) { return; } @@ -214,13 +229,11 @@ export class AuthenticationService extends Bootstrap { this.authProviderService.requestAuthProvider.addHandler(this.requestAuthProviderHandler); } - load(): void {} - private async authSessionAction(data: ISessionAction | null, contexts: IExecutionContextProvider) { const action = contexts.getContext(sessionActionContext); if (isAutoLoginSessionAction(data)) { - const user = await this.userInfoResource.finishFederatedAuthentication(data['auth-id'], false); + const user = await this.userInfoResource.autoLogin(data['auth-id'], false); if (user) { //we request this method/request bc login form can be opened automatically. diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.m.css b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.m.css deleted file mode 100644 index 1ed904ac9b..0000000000 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.m.css +++ /dev/null @@ -1,40 +0,0 @@ -.wrapper { - min-height: 520px !important; - max-height: max(100vh - 48px, 520px) !important; -} -.submittingForm { - overflow: auto; - margin: auto; -} -.submittingForm, -.authProviderForm { - flex: 1; - display: flex; - flex-direction: column; -} -.tabList { - composes: theme-background-surface theme-text-on-surface from global; - justify-content: center; - position: sticky; - top: 0; - z-index: 1; -} -.tab { - &:global([aria-selected='true']) { - font-weight: 500 !important; - } -} -.authProviderForm { - flex-direction: column; - padding: 18px 24px; -} -.configurationsList { - overflow: auto; - margin-top: 12px; - margin-bottom: 12px; - max-height: 400px; -} -.errorMessage { - composes: theme-background-secondary theme-text-on-secondary from global; - flex: 1; -} diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.module.css b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.module.css new file mode 100644 index 0000000000..c125d609b9 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.module.css @@ -0,0 +1,49 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.wrapper { + min-height: 520px !important; + max-height: max(var(--app-height, 100vh) - 48px, 520px) !important; + + & .tabList { + justify-content: center; + position: sticky; + top: 0; + z-index: 1; + & .tab { + &:global([aria-selected='true']) { + font-weight: 500; + } + } + } +} +.submittingForm { + overflow: auto; + margin: auto; +} +.submittingForm, +.authProviderForm { + flex: 1; + display: flex; + flex-direction: column; +} +.authProviderForm { + flex-direction: column; + padding: 18px 24px; +} +.configurationsList { + overflow: auto; + margin-top: 12px; + max-height: 400px; +} +.errorMessage { + composes: theme-background-secondary theme-text-on-secondary from global; + flex: 1; +} +.tooManySessionsCheckbox { + margin-bottom: 4px; +} diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index 4da36264eb..afb2444280 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -1,19 +1,20 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled from 'reshadow'; -import { AuthProvider, AuthProviderConfiguration, UserInfoResource } from '@cloudbeaver/core-authentication'; +import { type AuthProvider, type AuthProviderConfiguration, UserInfoResource } from '@cloudbeaver/core-authentication'; import { + Checkbox, CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, + Container, ErrorMessage, Form, getComputed, @@ -22,26 +23,26 @@ import { TextPlaceholder, useErrorDetails, useS, - useStyles, useTranslate, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogComponent } from '@cloudbeaver/core-dialogs'; -import { BASE_TAB_STYLES, Tab, TabList, TabsState, TabTitle, UNDERLINE_TAB_BIG_STYLES, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui'; - -import { AuthenticationService } from '../AuthenticationService'; -import type { IAuthOptions } from '../IAuthOptions'; -import style from './AuthDialog.m.css'; -import { AuthDialogFooter } from './AuthDialogFooter'; -import { AuthProviderForm } from './AuthProviderForm/AuthProviderForm'; -import { ConfigurationsList } from './AuthProviderForm/ConfigurationsList'; -import { FEDERATED_AUTH } from './FEDERATED_AUTH'; -import { getAuthProviderTabId, useAuthDialogState } from './useAuthDialogState'; +import { CommonDialogService, type DialogComponent } from '@cloudbeaver/core-dialogs'; +import { Tab, TabList, TabsState, TabTitle } from '@cloudbeaver/core-ui'; + +import { AuthenticationService } from '../AuthenticationService.js'; +import type { IAuthOptions } from '../IAuthOptions.js'; +import style from './AuthDialog.module.css'; +import { AuthDialogFooter } from './AuthDialogFooter.js'; +import { AuthProviderForm } from './AuthProviderForm/AuthProviderForm.js'; +import { ConfigurationsList } from './AuthProviderForm/ConfigurationsList.js'; +import { FEDERATED_AUTH } from './FEDERATED_AUTH.js'; +import { getAuthProviderTabId, useAuthDialogState } from './useAuthDialogState.js'; export const AuthDialog: DialogComponent = observer(function AuthDialog({ payload: { providerId, configurationId, linkUser = false, accessRequest = false }, options, rejectDialog, + resolveDialog, }) { const styles = useS(style); const dialogData = useAuthDialogState(accessRequest, providerId, configurationId); @@ -86,8 +87,11 @@ export const AuthDialog: DialogComponent = observer(function } async function login(linkUser: boolean, provider?: AuthProvider, configuration?: AuthProviderConfiguration) { - await dialogData.login(linkUser, provider, configuration); - rejectDialog(); + try { + await dialogData.login(linkUser, provider, configuration); + + resolveDialog(); + } catch {} } function navToSettings() { @@ -138,14 +142,19 @@ export const AuthDialog: DialogComponent = observer(function ); } - return styled(useStyles(BASE_TAB_STYLES, UNDERLINE_TAB_STYLES, UNDERLINE_TAB_BIG_STYLES))( + return ( { - state.setTabId(tabData.tabId); + state.switchAuthMode(tabData.tabId); }} > - + = observer(function subTitle={subTitle} onReject={options?.persistent ? undefined : rejectDialog} /> - + {showTabs && ( - + {dialogData.providers .map(provider => { if (provider.configurable) { @@ -202,7 +211,7 @@ export const AuthDialog: DialogComponent = observer(function disabled={dialogData.authenticating} onClick={() => { state.setActiveProvider(null, null); - state.setTabId(FEDERATED_AUTH); + state.switchAuthMode(FEDERATED_AUTH); }} > {translate('authentication_auth_federated')} @@ -226,9 +235,25 @@ export const AuthDialog: DialogComponent = observer(function )} - {!federate && ( - - login(linkUser)}> + + + {state.isTooManySessions && ( + { + state.forceSessionsLogout = e.currentTarget.checked; + }} + /> + )} + login(linkUser)} + > {errorDetails.name && ( = observer(function /> )} - - )} + + - , + ); }); diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.module.css b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.module.css new file mode 100644 index 0000000000..41150d4255 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.module.css @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.footerContainer { + display: flex; + width: min-content; + flex: 1; + align-items: center; + justify-content: flex-end; +} + +.footerContainer > :not(:first-child) { + margin-left: 16px; +} + +.button { + flex: 0 0 auto; +} diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx index a10d97bf84..508b47c935 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx @@ -1,30 +1,15 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled, { css } from 'reshadow'; -import { Button, useTranslate } from '@cloudbeaver/core-blocks'; +import { Button, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; -const styles = css` - footer-container { - display: flex; - width: min-content; - flex: 1; - align-items: center; - justify-content: flex-end; - } - footer-container > :not(:first-child) { - margin-left: 16px; - } - Button { - flex: 0 0 auto; - } -`; +import styles from './AuthDialogFooter.module.css'; export interface Props extends React.PropsWithChildren { authAvailable: boolean; @@ -34,13 +19,16 @@ export interface Props extends React.PropsWithChildren { export const AuthDialogFooter = observer(function AuthDialogFooter({ authAvailable, isAuthenticating, onLogin, children }) { const translate = useTranslate(); + const style = useS(styles); - return styled(styles)( - + return ( +
{children} - - , + {authAvailable && ( + + )} +
); }); diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts index f6d0e9fab0..207f093cde 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,10 +8,10 @@ import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import type { IAuthOptions } from '../IAuthOptions'; -import { AuthDialog } from './AuthDialog'; +import type { IAuthOptions } from '../IAuthOptions.js'; +import { AuthDialog } from './AuthDialog.js'; -@injectable() +@injectable(() => [CommonDialogService]) export class AuthDialogService { get isPersistent(): boolean { return this.persistent; diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx index eec05b6266..44f1f1e806 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import type { AuthProvider, AuthProviderConfiguration, IAuthCredentials } from '@cloudbeaver/core-authentication'; -import { Combobox, Group, InputField, useFocus } from '@cloudbeaver/core-blocks'; +import { Select, Group, InputField, useFocus } from '@cloudbeaver/core-blocks'; interface Props { provider: AuthProvider; @@ -17,19 +17,19 @@ interface Props { authenticate: boolean; } -export const AuthProviderForm = observer(function AuthProviderForm({ provider, credentials, authenticate }) { +export const AuthProviderForm = observer(function AuthProviderForm({ provider, configuration, credentials, authenticate }) { const [elementRef] = useFocus({ focusFirstChild: true }); function handleProfileSelect() { credentials.credentials = {}; } - const profile = provider.credentialProfiles[credentials.profile as any as number]; + const profile = provider.credentialProfiles[credentials.profile as any as number]!; return ( {provider.credentialProfiles.length > 1 && ( - (function AuthProviderForm({ prov parameter => parameter.user && ( {parameter.displayName} diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.module.css b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.module.css new file mode 100644 index 0000000000..7764c60ff3 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.module.css @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.filter { + margin: 0 24px 4px 24px; +} + +.cell { + composes: theme-border-color-secondary from global; + border-bottom: 1px solid; + padding: 0 16px; +} + +.iconOrImage { + width: 100%; + height: 100%; +} diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx index b5dcf7b703..9d0e9dea55 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/ConfigurationsList.tsx @@ -1,68 +1,42 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useState } from 'react'; -import styled, { css } from 'reshadow'; -import { AuthProvider, AuthProviderConfiguration, AuthProvidersResource, comparePublicAuthConfigurations } from '@cloudbeaver/core-authentication'; +import { + type AuthProvider, + type AuthProviderConfiguration, + AuthProvidersResource, + comparePublicAuthConfigurations, +} from '@cloudbeaver/core-authentication'; import { Button, Cell, + Clickable, + Container, Filter, getComputed, IconOrImage, Link, Loader, + s, TextPlaceholder, Translate, usePromiseState, - useStyles, + useS, useTranslate, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import type { ITask } from '@cloudbeaver/core-executor'; import type { UserInfo } from '@cloudbeaver/core-sdk'; -import { ServerConfigurationAdministrationNavService } from '@cloudbeaver/plugin-administration'; - -import { AuthenticationService } from '../../AuthenticationService'; - -const styles = css` - container { - display: flex; - flex-direction: column; - overflow: auto; - flex: 1; - } - Filter { - margin: 0 24px 12px 24px; - } - list { - overflow: auto; - } - Cell { - composes: theme-border-color-secondary from global; - border-bottom: 1px solid; - padding: 0 16px; - } - IconOrImage { - width: 100%; - height: 100%; - } - center { - margin: auto; - } -`; -const loaderStyle = css` - ExceptionMessage { - padding: 24px; - } -`; +import { AuthenticationService } from '../../AuthenticationService.js'; +import styles from './ConfigurationsList.module.css'; interface IProviderConfiguration { provider: AuthProvider; @@ -88,10 +62,9 @@ export const ConfigurationsList = observer(function ConfigurationsList({ onClose, className, }) { - const serverConfigurationAdministrationNavService = useService(ServerConfigurationAdministrationNavService); const authenticationService = useService(AuthenticationService); const translate = useTranslate(); - const style = useStyles(styles); + const style = useS(styles); const [search, setSearch] = useState(''); const authTaskState = usePromiseState(authTask); @@ -118,11 +91,6 @@ export const ConfigurationsList = observer(function ConfigurationsList({ return target.toUpperCase().includes(search.toUpperCase()); }); - function navToSettings() { - onClose?.(); - serverConfigurationAdministrationNavService.navToSettings(); - } - function navToIdentityProvidersSettings() { onClose?.(); authenticationService.configureIdentityProvider?.(); @@ -138,45 +106,55 @@ export const ConfigurationsList = observer(function ConfigurationsList({ } if (activeProvider && activeConfiguration) { - return styled(style)( - - -
+ return ( + + + {providerDisabled ? ( - - {translate('plugin_authentication_authentication_method_disabled')} - {authenticationService.configureIdentityProvider && {translate('ui_configure')}} - + {translate('plugin_authentication_authentication_method_disabled')} ) : ( - )} -
+
- , + ); } - return styled(style)( - + return ( + {configurations.length >= 10 && ( - + + + )} - + {filteredConfigurations.map(({ provider, configuration }) => { const icon = configuration.iconURL || provider.icon; const title = `${configuration.displayName}\n${configuration.description || ''}`; return ( login(false, provider, configuration)}> - : undefined} description={configuration.description}> - {configuration.displayName} - + + : undefined} + description={configuration.description} + > + {configuration.displayName} + + ); })} - - - , + + + ); }); diff --git a/webapp/packages/plugin-authentication/src/Dialog/FEDERATED_AUTH.ts b/webapp/packages/plugin-authentication/src/Dialog/FEDERATED_AUTH.ts index 2fd0dd35dc..53a9761774 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/FEDERATED_AUTH.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/FEDERATED_AUTH.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts b/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts index 43529d781a..f6c085e6e3 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,16 +9,23 @@ import { action, computed, observable, untracked } from 'mobx'; import { useEffect } from 'react'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { AuthInfoService, AuthProvider, AuthProviderConfiguration, AuthProvidersResource, IAuthCredentials } from '@cloudbeaver/core-authentication'; -import { useObservableRef, useResource } from '@cloudbeaver/core-blocks'; +import { + AuthInfoService, + type AuthProvider, + type AuthProviderConfiguration, + AuthProvidersResource, + type IAuthCredentials, +} from '@cloudbeaver/core-authentication'; +import { ConfirmationDialog, useObservableRef, useResource } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { NotificationService, UIError } from '@cloudbeaver/core-events'; import type { ITask } from '@cloudbeaver/core-executor'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import type { UserInfo } from '@cloudbeaver/core-sdk'; -import { isArraysEqual } from '@cloudbeaver/core-utils'; +import { EServerErrorCode, GQLError, type UserInfo } from '@cloudbeaver/core-sdk'; +import { errorOf, isArraysEqual } from '@cloudbeaver/core-utils'; -import { FEDERATED_AUTH } from './FEDERATED_AUTH'; +import { FEDERATED_AUTH } from './FEDERATED_AUTH.js'; interface IData { state: IState; @@ -33,7 +40,7 @@ interface IData { tabIds: string[]; login: (linkUser: boolean, provider?: AuthProvider, configuration?: AuthProviderConfiguration) => Promise; - loginFederated: (provider: AuthProvider, configuration: AuthProviderConfiguration, onClose?: () => void) => Promise; + federatedLogin: (provider: AuthProvider, configuration: AuthProviderConfiguration) => Promise; } interface IState { @@ -42,9 +49,11 @@ interface IState { activeConfiguration: AuthProviderConfiguration | null; credentials: IAuthCredentials; tabIds: string[]; - - setTabId: (tabId: string | null) => void; + isTooManySessions: boolean; + forceSessionsLogout: boolean; + switchAuthMode: (tabId: string | null, resetError?: boolean) => void; setActiveProvider: (provider: AuthProvider | null, configuration: AuthProviderConfiguration | null) => void; + resetErrorState: VoidFunction; } export function useAuthDialogState(accessRequest: boolean, providerId: string | null, configurationId?: string): IData { @@ -52,17 +61,13 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | const administrationScreenService = useService(AdministrationScreenService); const authInfoService = useService(AuthInfoService); const notificationService = useService(NotificationService); + const commonDialogService = useService(CommonDialogService); - const primaryId = authProvidersResource.resource.getPrimary(); const adminPageActive = administrationScreenService.isAdministrationPageActive; const providers = authProvidersResource.data.filter(notEmptyProvider).sort(compareProviders); const activeProviders = providers.filter(provider => { - if (provider.id === primaryId && adminPageActive && accessRequest) { - return true; - } - - if (provider.federated || provider.trusted || provider.private) { + if (provider.federated || provider.trusted || provider.private || provider.authHidden) { return false; } @@ -115,12 +120,27 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | profile: '0', credentials: {}, }, - setTabId(tabId: string | null): void { + isTooManySessions: false, + forceSessionsLogout: false, + switchAuthMode(tabId: string | null, resetError = true): void { + if (tabId !== null && tabId === this.tabId) { + return; + } + if (tabIds.includes(tabId as any)) { this.tabId = tabId; } else { this.tabId = tabIds[0] ?? null; } + + if (resetError) { + this.resetErrorState(); + } + }, + resetErrorState(): void { + this.isTooManySessions = false; + this.forceSessionsLogout = false; + data.exception = null; }, setActiveProvider(provider: AuthProvider | null, configuration: AuthProviderConfiguration | null): void { const providerChanged = this.activeProvider?.id !== provider?.id; @@ -136,12 +156,12 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | if (provider) { if (provider.federated) { - this.setTabId(FEDERATED_AUTH); + this.switchAuthMode(FEDERATED_AUTH); } else { - this.setTabId(getAuthProviderTabId(provider, configuration)); + this.switchAuthMode(getAuthProviderTabId(provider, configuration)); } } else { - this.setTabId(null); + this.switchAuthMode(null, false); } }, }), @@ -150,7 +170,11 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | activeProvider: observable.ref, activeConfiguration: observable.ref, credentials: observable, - setActiveProvider: action, + isTooManySessions: observable.ref, + forceSessionsLogout: observable.ref, + switchAuthMode: action.bound, + setActiveProvider: action.bound, + resetErrorState: action.bound, }, false, ); @@ -170,9 +194,6 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | get configure(): boolean { if (state.activeProvider) { - if (this.adminPageActive && authProvidersResource.resource.isPrimary(state.activeProvider.id)) { - return false; - } return !authProvidersResource.resource.isAuthEnabled(state.activeProvider.id); } return false; @@ -185,24 +206,53 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | return; } + if (state.isTooManySessions && state.forceSessionsLogout) { + const result = await commonDialogService.open(ConfirmationDialog, { + title: 'authentication_auth_force_session_logout_popup_title', + message: 'authentication_auth_force_session_logout_popup_message', + }); + + if (result === DialogueStateResult.Rejected) { + throw new UIError('Force session logout confirmation dialog rejected'); + } + } + this.authenticating = true; + state.isTooManySessions = false; + try { this.state.setActiveProvider(provider, configuration ?? null); - const loginTask = authInfoService.login(provider.id, { - configurationId: configuration?.id, - credentials: state.credentials, - linkUser, - }); - this.authTask = loginTask; - - await loginTask; + if (provider.federated && configuration) { + await this.federatedLogin(provider, configuration); + } else { + await authInfoService.login(provider.id, { + configurationId: configuration?.id, + credentials: { + ...state.credentials, + credentials: { + ...state.credentials.credentials, + user: state.credentials.credentials['user']?.trim(), + password: state.credentials.credentials['password']?.trim(), + }, + }, + forceSessionsLogout: state.forceSessionsLogout, + linkUser, + }); + } } catch (exception: any) { + const gqlError = errorOf(exception, GQLError); + + if (gqlError?.errorCode === EServerErrorCode.tooManySessions) { + state.isTooManySessions = true; + } + if (this.destroyed) { notificationService.logException(exception, 'Login failed'); } else { this.exception = exception; } + throw exception; } finally { this.authTask = null; @@ -210,9 +260,20 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string | if (provider.federated) { this.state.setActiveProvider(null, null); - this.state.setTabId(FEDERATED_AUTH); + this.state.switchAuthMode(FEDERATED_AUTH, false); } } + + return; + }, + async federatedLogin(provider: AuthProvider, configuration: AuthProviderConfiguration): Promise { + this.authTask = authInfoService.federatedLogin(provider.id, { + configurationId: configuration.id, + forceSessionsLogout: state.forceSessionsLogout, + linkUser: false, + }); + + await this.authTask; }, }), { diff --git a/webapp/packages/plugin-authentication/src/IAuthOptions.ts b/webapp/packages/plugin-authentication/src/IAuthOptions.ts index 1456c71860..e9e2baf78a 100644 --- a/webapp/packages/plugin-authentication/src/IAuthOptions.ts +++ b/webapp/packages/plugin-authentication/src/IAuthOptions.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-authentication/src/IAutoLoginSessionAction.ts b/webapp/packages/plugin-authentication/src/IAutoLoginSessionAction.ts index d2a3c87b30..2d48a35895 100644 --- a/webapp/packages/plugin-authentication/src/IAutoLoginSessionAction.ts +++ b/webapp/packages/plugin-authentication/src/IAutoLoginSessionAction.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts index 814717d5fd..538b4673c6 100644 --- a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts @@ -1,75 +1,63 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { AuthInfoService } from '@cloudbeaver/core-authentication'; +import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { DATA_CONTEXT_MENU, MenuBaseItem, MenuService } from '@cloudbeaver/core-view'; +import { MenuBaseItem, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu'; -import { AuthenticationService } from './AuthenticationService'; +import { AuthenticationService } from './AuthenticationService.js'; -@injectable() +@injectable(() => [ServerConfigResource, AuthenticationService, UserInfoResource, MenuService]) export class PluginBootstrap extends Bootstrap { constructor( private readonly serverConfigResource: ServerConfigResource, private readonly authenticationService: AuthenticationService, - private readonly authInfoService: AuthInfoService, + private readonly userInfoResource: UserInfoResource, private readonly menuService: MenuService, ) { super(); } - register(): void { + override register(): void { + const LOGIN_ITEM = new MenuBaseItem( + { + id: 'login', + label: 'authentication_login', + tooltip: 'authentication_login', + }, + { onSelect: () => this.authenticationService.authUser(null, false) }, + ); + const LOGOUT_ITEM = new MenuBaseItem( + { + id: 'logout', + label: 'authentication_logout', + tooltip: 'authentication_logout', + }, + { onSelect: () => this.authenticationService.logout() }, + ); this.menuService.addCreator({ - isApplicable: context => context.get(DATA_CONTEXT_MENU) === TOP_NAV_BAR_SETTINGS_MENU, + menus: [TOP_NAV_BAR_SETTINGS_MENU], getItems: (context, items) => { - if (this.serverConfigResource.enabledAuthProviders.length > 0 && !this.authInfoService.userInfo) { - return [ - ...items, - new MenuBaseItem( - { - id: 'login', - label: 'authentication_login', - tooltip: 'authentication_login', - }, - { onSelect: () => this.authenticationService.authUser(null, false) }, - ), - ]; + if (this.serverConfigResource.enabledAuthProviders.length > 0 && this.userInfoResource.isAnonymous()) { + return [...items, LOGIN_ITEM]; } - if (this.authInfoService.userInfo) { - return [ - ...items, - new MenuBaseItem( - { - id: 'logout', - label: 'authentication_logout', - tooltip: 'authentication_logout', - }, - { onSelect: () => this.authenticationService.logout() }, - ), - ]; + if (this.userInfoResource.isAuthenticated()) { + return [...items, LOGOUT_ITEM]; } return items; }, orderItems: (context, items) => { - const index = items.findIndex(item => item.id === 'logout' || item.id === 'login'); - - if (index > -1) { - const item = items.splice(index, 1); - items.push(item[0]); - } - + items.push(...menuExtractItems(items, [LOGIN_ITEM, LOGOUT_ITEM])); return items; }, }); } - - load(): void | Promise {} } diff --git a/webapp/packages/plugin-authentication/src/UserLoadingErrorDialogBootstrap.ts b/webapp/packages/plugin-authentication/src/UserLoadingErrorDialogBootstrap.ts index 5820b7219e..1e897a4b7a 100644 --- a/webapp/packages/plugin-authentication/src/UserLoadingErrorDialogBootstrap.ts +++ b/webapp/packages/plugin-authentication/src/UserLoadingErrorDialogBootstrap.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -11,7 +11,7 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -@injectable() +@injectable(() => [UserInfoResource, CommonDialogService, NotificationService]) export class UserLoadingErrorDialogBootstrap extends Bootstrap { constructor( private readonly userInfoResource: UserInfoResource, @@ -22,9 +22,6 @@ export class UserLoadingErrorDialogBootstrap extends Bootstrap { userInfoResource.onException.addHandler(this.handleException.bind(this)); } - register(): void | Promise {} - load(): void | Promise {} - private async handleException(exception: Error) { this.notificationService.logException(exception, 'plugin_authentication_user_loading_error'); const result = await this.commonDialogService.open(ConfirmationDialog, { diff --git a/webapp/packages/plugin-authentication/src/index.ts b/webapp/packages/plugin-authentication/src/index.ts index 998e8e8c93..99c2ce3e2a 100644 --- a/webapp/packages/plugin-authentication/src/index.ts +++ b/webapp/packages/plugin-authentication/src/index.ts @@ -1,5 +1,14 @@ -import { manifest } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 * from './AuthenticationService'; -export * from './Dialog/AuthDialogService'; +import './module.js'; +import { manifest } from './manifest.js'; + +export * from './AuthenticationService.js'; +export * from './Dialog/AuthDialogService.js'; export default manifest; diff --git a/webapp/packages/plugin-authentication/src/isAutoLoginSessionAction.ts b/webapp/packages/plugin-authentication/src/isAutoLoginSessionAction.ts index 2cd07a11ca..84a4ddb557 100644 --- a/webapp/packages/plugin-authentication/src/isAutoLoginSessionAction.ts +++ b/webapp/packages/plugin-authentication/src/isAutoLoginSessionAction.ts @@ -1,11 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IAutoLoginSessionAction } from './IAutoLoginSessionAction'; +import type { IAutoLoginSessionAction } from './IAutoLoginSessionAction.js'; export function isAutoLoginSessionAction(obj: any): obj is IAutoLoginSessionAction { return obj && 'action' in obj && obj.action === 'auto-login'; diff --git a/webapp/packages/plugin-authentication/src/locales/en.ts b/webapp/packages/plugin-authentication/src/locales/en.ts index c093256005..bcd002eb35 100644 --- a/webapp/packages/plugin-authentication/src/locales/en.ts +++ b/webapp/packages/plugin-authentication/src/locales/en.ts @@ -2,9 +2,14 @@ export default [ ['authentication_login_dialog_title', 'Authentication'], ['authentication_login', 'Login'], ['authentication_logout', 'Logout'], + ['authentication_logout_error', "Can't logout"], ['authentication_authenticate', 'Authenticate'], ['authentication_authorizing', 'Authorizing...'], ['authentication_auth_federated', 'Federated'], + ['authentication_auth_force_session_logout', 'Force all other sessions logout'], + ['authentication_auth_force_session_logout_popup_title', 'Logout other sessions'], + ['authentication_auth_force_session_logout_popup_message', 'All users will be logged out from all of the devices. Do you want to continue?'], + ['authentication_auth_force_session_logout_checkbox_tooltip', 'To apply this option you need to login again'], ['authentication_auth_additional', 'Additional'], ['authentication_select_provider', 'Select available provider'], ['authentication_configure', 'Please configure authentication methods'], @@ -13,13 +18,14 @@ export default [ ['authentication_identity_provider_search_placeholder', 'Search for configuration name or description...'], ['authentication_identity_provider_dialog_subtitle', 'Choose configuration you want to sign in with'], - ['authentication_user_name', 'User name'], + ['authentication_user_name', 'Username'], + ['authentication_user_name_description', "User's identifier is stored in lowercase"], ['authentication_user_role', 'Role'], ['authentication_user_credentials', 'Credentials'], ['authentication_user_meta_parameters', 'Parameters'], ['authentication_team_meta_parameters', 'Parameters'], - ['authentication_user_password', 'User password'], - ['authentication_user_password_repeat', 'Repeat password'], + ['authentication_user_password', 'User Password'], + ['authentication_user_password_repeat', 'Repeat Password'], ['authentication_user_team', 'User team'], ['authentication_user_status', 'User status'], ['authentication_user_enabled', 'Enabled'], diff --git a/webapp/packages/plugin-authentication/src/locales/fr.ts b/webapp/packages/plugin-authentication/src/locales/fr.ts new file mode 100644 index 0000000000..1b2ddf496d --- /dev/null +++ b/webapp/packages/plugin-authentication/src/locales/fr.ts @@ -0,0 +1,56 @@ +export default [ + ['authentication_login_dialog_title', 'Authentification'], + ['authentication_login', 'Connexion'], + ['authentication_logout', 'Déconnexion'], + ['authentication_logout_error', 'Impossible de se déconnecter'], + ['authentication_authenticate', "S'authentifier"], + ['authentication_authorizing', 'Autorisation...'], + ['authentication_auth_federated', 'Fédéré'], + ['authentication_auth_force_session_logout', 'Forcer la déconnexion de toutes les autres sessions'], + ['authentication_auth_force_session_logout_popup_title', 'Déconnecter les autres sessions'], + [ + 'authentication_auth_force_session_logout_popup_message', + 'Tous les utilisateurs seront déconnectés de tous les appareils. Voulez-vous continuer ?', + ], + ['authentication_auth_force_session_logout_checkbox_tooltip', 'Pour appliquer cette option, vous devez vous reconnecter'], + ['authentication_auth_additional', 'Supplémentaire'], + ['authentication_select_provider', 'Sélectionner un fournisseur disponible'], + ['authentication_configure', "Veuillez configurer les méthodes d'authentification"], + ['authentication_provider_disabled', "La méthode d'authentification est désactivée"], + ['authentication_request_token', 'Une authentification supplémentaire est requise'], + ['authentication_identity_provider_search_placeholder', 'Rechercher le nom ou la description de la configuration...'], + ['authentication_identity_provider_dialog_subtitle', 'Choisissez la configuration avec laquelle vous souhaitez vous connecter'], + + ['authentication_user_name', "Nom d'utilisateur"], + ['authentication_user_name_description', "User's identifier is stored in lowercase"], + ['authentication_user_role', 'Rôle'], + ['authentication_user_credentials', 'Identifiants'], + ['authentication_user_meta_parameters', 'Paramètres'], + ['authentication_team_meta_parameters', 'Paramètres'], + ['authentication_user_password', "Mot de passe de l'utilisateur"], + ['authentication_user_password_repeat', 'Répéter le mot de passe'], + ['authentication_user_team', "Équipe de l'utilisateur"], + ['authentication_user_status', "Statut de l'utilisateur"], + ['authentication_user_enabled', 'Activé'], + ['authentication_user_login_not_set', "Le nom d'utilisateur ne peut pas être vide"], + ['authentication_user_team_not_set', 'Au moins une équipe doit être sélectionnée'], + ['authentication_user_role_not_set', '{alias:authentication_user_role} est requis'], + ['authentication_user_password_not_set', '{alias:authentication_user_password} est requis'], + ['authentication_user_passwords_not_match', 'Les mots de passe ne correspondent pas'], + ['authentication_user_login_already_exists', 'Un utilisateur avec ce nom existe déjà'], + ['authentication_user_login_cant_be_used', 'Désolé, ce nom ne peut pas être utilisé'], + ['authentication_user_icon_tooltip', 'Utilisateur'], + ['authentication_team_icon_tooltip', 'Équipe'], + + ['plugin_authentication_user_loading_error', 'Impossible de charger les données utilisateur'], + ['plugin_authentication_loading_error_dialog_title', "Erreur d'authentification"], + [ + 'plugin_authentication_loading_error_dialog_message', + 'Une erreur est survenue lors du chargement des données utilisateur. Voulez-vous vous déconnecter ?', + ], + + [ + 'plugin_authentication_authentication_method_disabled', + "La méthode d'authentification est désactivée, veuillez configurer les méthodes d'authentification dans les paramètres", + ], +]; diff --git a/webapp/packages/plugin-authentication/src/locales/it.ts b/webapp/packages/plugin-authentication/src/locales/it.ts index 00115b741c..bff9659c62 100644 --- a/webapp/packages/plugin-authentication/src/locales/it.ts +++ b/webapp/packages/plugin-authentication/src/locales/it.ts @@ -2,12 +2,18 @@ export default [ ['authentication_login_dialog_title', 'Autenticazione'], ['authentication_login', 'Login'], ['authentication_logout', 'Logout'], + ['authentication_logout_error', "Can't logout"], ['authentication_authenticate', 'Autentica'], ['authentication_authorizing', 'Authorizing...'], ['authentication_auth_federated', 'Federated'], + ['authentication_auth_force_session_logout', 'Force all other sessions logout'], + ['authentication_auth_force_session_logout_popup_title', 'Logout other sessions'], + ['authentication_auth_force_session_logout_popup_message', 'All users will be logged out from all of the devices. Do you want to continue?'], + ['authentication_auth_force_session_logout_checkbox_tooltip', 'To apply this option you need to login again'], ['authentication_auth_additional', 'Additional'], ['authentication_request_token', 'Autenticazione addizionale richiesta'], ['authentication_user_name', 'Nome utente'], + ['authentication_user_name_description', "User's identifier is stored in lowercase"], ['authentication_user_role', 'Role'], ['authentication_user_credentials', 'Credenziali'], ['authentication_user_meta_parameters', 'Parameters'], diff --git a/webapp/packages/plugin-authentication/src/locales/ru.ts b/webapp/packages/plugin-authentication/src/locales/ru.ts index 228e105af9..bdc636349c 100644 --- a/webapp/packages/plugin-authentication/src/locales/ru.ts +++ b/webapp/packages/plugin-authentication/src/locales/ru.ts @@ -2,18 +2,24 @@ export default [ ['authentication_login_dialog_title', 'Аутентификация'], ['authentication_login', 'Войти'], ['authentication_logout', 'Выйти'], + ['authentication_logout_error', 'Не удалось выйти'], ['authentication_authenticate', 'Аутентифицироваться'], ['authentication_authorizing', 'Авторизация...'], ['authentication_auth_federated', 'Федеративная'], + ['authentication_auth_force_session_logout', 'Принудительный выход из всех сессий'], + ['authentication_auth_force_session_logout_popup_message', 'Текущие сессии для всех пользователей будут прерваны на всех устройствах. Продолжить?'], + ['authentication_auth_force_session_logout_popup_title', 'Выход из всех сессий'], + ['authentication_auth_force_session_logout_checkbox_tooltip', 'Для применения этой опции вам нужно войти снова'], ['authentication_auth_additional', 'Дополнительная'], ['authentication_select_provider', 'Выберите способ аутентификации'], ['authentication_configure', 'Пожалуйста настройте способы аутентификации'], ['authentication_provider_disabled', 'Способ аутентификации отключен'], ['authentication_request_token', 'Необходима дополнительная аутентификация'], ['authentication_identity_provider_search_placeholder', 'Поиск по имени или описанию конфигурации...'], - ['authentication_identity_provider_dialog_subtitle', 'Выберите конфигурацию с помощью которой вы хотите войти'], + ['authentication_identity_provider_dialog_subtitle', 'Выберите конфигурацию, с помощью которой вы хотите войти'], ['authentication_user_name', 'Имя'], + ['authentication_user_name_description', 'Идентификатор пользователя хранится в нижнем регистре'], ['authentication_user_role', 'Роль'], ['authentication_user_credentials', 'Учетные данные'], ['authentication_user_meta_parameters', 'Параметры'], diff --git a/webapp/packages/plugin-authentication/src/locales/vi.ts b/webapp/packages/plugin-authentication/src/locales/vi.ts new file mode 100644 index 0000000000..f23c870b6a --- /dev/null +++ b/webapp/packages/plugin-authentication/src/locales/vi.ts @@ -0,0 +1,47 @@ +export default [ + ['authentication_login_dialog_title', 'Xác thực'], + ['authentication_login', 'Đăng nhập'], + ['authentication_logout', 'Đăng xuất'], + ['authentication_logout_error', 'Không thể đăng xuất'], + ['authentication_authenticate', 'Xác thực'], + ['authentication_authorizing', 'Đang xác thực...'], + ['authentication_auth_federated', 'Liên kết'], + ['authentication_auth_force_session_logout', 'Buộc đăng xuất tất cả các phiên khác'], + ['authentication_auth_force_session_logout_popup_title', 'Đăng xuất các phiên khác'], + ['authentication_auth_force_session_logout_popup_message', 'Tất cả người dùng sẽ bị đăng xuất khỏi tất cả thiết bị. Bạn có muốn tiếp tục không?'], + ['authentication_auth_force_session_logout_checkbox_tooltip', 'Để áp dụng tùy chọn này, bạn cần đăng nhập lại'], + ['authentication_auth_additional', 'Bổ sung'], + ['authentication_select_provider', 'Chọn nhà cung cấp khả dụng'], + ['authentication_configure', 'Vui lòng cấu hình các phương thức xác thực'], + ['authentication_provider_disabled', 'Phương thức xác thực đã bị tắt'], + ['authentication_request_token', 'Yêu cầu xác thực bổ sung'], + ['authentication_identity_provider_search_placeholder', 'Tìm kiếm tên cấu hình hoặc mô tả...'], + ['authentication_identity_provider_dialog_subtitle', 'Chọn cấu hình bạn muốn sử dụng để đăng nhập'], + ['authentication_user_name', 'Tên người dùng'], + ['authentication_user_name_description', 'Định danh của người dùng được lưu dưới dạng chữ thường'], + ['authentication_user_role', 'Vai trò'], + ['authentication_user_credentials', 'Thông tin xác thực'], + ['authentication_user_meta_parameters', 'Tham số'], + ['authentication_team_meta_parameters', 'Tham số'], + ['authentication_user_password', 'Mật khẩu Người dùng'], + ['authentication_user_password_repeat', 'Nhập lại Mật khẩu'], + ['authentication_user_team', 'Nhóm Người dùng'], + ['authentication_user_status', 'Trạng thái Người dùng'], + ['authentication_user_enabled', 'Đã bật'], + ['authentication_user_login_not_set', 'Đăng nhập không được để trống'], + ['authentication_user_team_not_set', 'Phải chọn ít nhất một nhóm'], + ['authentication_user_role_not_set', '{alias:authentication_user_role} là bắt buộc'], + ['authentication_user_password_not_set', '{alias:authentication_user_password} là bắt buộc'], + ['authentication_user_passwords_not_match', 'Mật khẩu không khớp'], + ['authentication_user_login_already_exists', 'Người dùng với tên đã tồn tại'], + ['authentication_user_login_cant_be_used', 'Xin lỗi, tên không thể sử dụng'], + ['authentication_user_icon_tooltip', 'Người dùng'], + ['authentication_team_icon_tooltip', 'Nhóm'], + ['plugin_authentication_user_loading_error', 'Không thể tải dữ liệu người dùng'], + ['plugin_authentication_loading_error_dialog_title', 'Lỗi xác thực'], + ['plugin_authentication_loading_error_dialog_message', 'Đã xảy ra lỗi khi tải dữ liệu người dùng. Bạn có muốn đăng xuất không?'], + [ + 'plugin_authentication_authentication_method_disabled', + 'Phương thức xác thực đã bị tắt, vui lòng cấu hình các phương thức xác thực trong cài đặt', + ], +]; diff --git a/webapp/packages/plugin-authentication/src/locales/zh.ts b/webapp/packages/plugin-authentication/src/locales/zh.ts index 97476a707d..880a8f106b 100644 --- a/webapp/packages/plugin-authentication/src/locales/zh.ts +++ b/webapp/packages/plugin-authentication/src/locales/zh.ts @@ -1,11 +1,16 @@ export default [ ['authentication_login_dialog_title', '认证'], ['authentication_login', '登录'], - ['authentication_logout', '登出'], + ['authentication_logout', '注销'], + ['authentication_logout_error', '无法注销'], ['authentication_authenticate', '认证'], - ['authentication_authorizing', 'Authorizing...'], + ['authentication_authorizing', '认证中...'], ['authentication_auth_federated', '联合认证'], - ['authentication_auth_additional', 'Additional'], + ['authentication_auth_force_session_logout', '强制注销所有其他会话'], + ['authentication_auth_force_session_logout_popup_title', '注销其他会话'], + ['authentication_auth_force_session_logout_popup_message', '所有用户都将从所有设备中注销。是否要继续?'], + ['authentication_auth_force_session_logout_checkbox_tooltip', '要应用此选项,您需要再次登录'], + ['authentication_auth_additional', '更多'], ['authentication_select_provider', '选择可用提供者'], ['authentication_configure', '请配置认证方法'], ['authentication_provider_disabled', '认证方法已禁用'], @@ -14,25 +19,26 @@ export default [ ['authentication_identity_provider_dialog_subtitle', '选择您要用于登录的配置'], ['authentication_user_name', '用户名称'], + ['authentication_user_name_description', "User's identifier is stored in lowercase"], ['authentication_user_role', 'Role'], ['authentication_user_credentials', '凭据'], ['authentication_user_meta_parameters', '参数'], ['authentication_team_meta_parameters', '参数'], ['authentication_user_password', '用户密码'], ['authentication_user_password_repeat', '重复密码'], - ['authentication_user_status', 'User status'], - ['authentication_user_enabled', 'Enabled'], + ['authentication_user_status', '用户状态'], + ['authentication_user_enabled', '可用'], ['authentication_user_login_not_set', '用户名不能为空'], - ['authentication_user_role_not_set', '{alias:authentication_user_role} is required'], + ['authentication_user_role_not_set', '{alias:authentication_user_role} 必须'], ['authentication_user_password_not_set', '密码是必须的'], ['authentication_user_passwords_not_match', '密码不匹配'], ['authentication_user_login_already_exists', '同名用户已存在'], ['authentication_user_login_cant_be_used', '抱歉,不能使用该名称'], ['authentication_user_icon_tooltip', '用户'], - ['plugin_authentication_user_loading_error', "Can't load user data"], - ['plugin_authentication_loading_error_dialog_title', 'Authentication error'], - ['plugin_authentication_loading_error_dialog_message', 'An error occurred while loading user data. Do you want to logout?'], + ['plugin_authentication_user_loading_error', '无法加载用户数据'], + ['plugin_authentication_loading_error_dialog_title', '认证错误'], + ['plugin_authentication_loading_error_dialog_message', '加载用户数据出错,是否注销?'], - ['plugin_authentication_authentication_method_disabled', 'Authentication method is disabled, please configure authentication methods in settings'], + ['plugin_authentication_authentication_method_disabled', '认证方式禁用,请在设置中设定认证方式'], ]; diff --git a/webapp/packages/plugin-authentication/src/manifest.ts b/webapp/packages/plugin-authentication/src/manifest.ts index d9aabb275c..4bb813e8de 100644 --- a/webapp/packages/plugin-authentication/src/manifest.ts +++ b/webapp/packages/plugin-authentication/src/manifest.ts @@ -1,22 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { AuthenticationLocaleService } from './AuthenticationLocaleService'; -import { AuthenticationService } from './AuthenticationService'; -import { AuthDialogService } from './Dialog/AuthDialogService'; -import { PluginBootstrap } from './PluginBootstrap'; -import { UserLoadingErrorDialogBootstrap } from './UserLoadingErrorDialogBootstrap'; - export const manifest: PluginManifest = { info: { name: 'Plugin Authentication', }, - - providers: [AuthenticationService, AuthDialogService, PluginBootstrap, AuthenticationLocaleService, UserLoadingErrorDialogBootstrap], }; diff --git a/webapp/packages/plugin-authentication/src/module.ts b/webapp/packages/plugin-authentication/src/module.ts new file mode 100644 index 0000000000..cc6a76b730 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/module.ts @@ -0,0 +1,30 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { UserLoadingErrorDialogBootstrap } from './UserLoadingErrorDialogBootstrap.js'; +import { PluginBootstrap } from './PluginBootstrap.js'; +import { AuthDialogService } from './Dialog/AuthDialogService.js'; +import { AuthenticationService } from './AuthenticationService.js'; +import { AuthenticationLocaleService } from './AuthenticationLocaleService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-authentication', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, AuthenticationLocaleService) + .addSingleton(Bootstrap, proxy(UserLoadingErrorDialogBootstrap)) + .addSingleton(Bootstrap, proxy(PluginBootstrap)) + .addSingleton(Bootstrap, proxy(AuthenticationService)) + .addSingleton(UserLoadingErrorDialogBootstrap) + .addSingleton(PluginBootstrap) + .addSingleton(AuthDialogService) + .addSingleton(AuthenticationService); + }, +}); diff --git a/webapp/packages/plugin-authentication/tsconfig.json b/webapp/packages/plugin-authentication/tsconfig.json index a9b692c1ca..0ed2b3d258 100644 --- a/webapp/packages/plugin-authentication/tsconfig.json +++ b/webapp/packages/plugin-authentication/tsconfig.json @@ -1,61 +1,62 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-administration/tsconfig.json" + "path": "../core-administration" }, { - "path": "../plugin-settings-menu/tsconfig.json" + "path": "../core-authentication" }, { - "path": "../core-administration/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-authentication/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-executor" }, { - "path": "../core-executor/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-routing" }, { - "path": "../core-routing/tsconfig.json" + "path": "../core-sdk" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-utils" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-view" }, { - "path": "../core-view/tsconfig.json" + "path": "../plugin-settings-menu" } ], "include": [ @@ -67,7 +68,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-browser/package.json b/webapp/packages/plugin-browser/package.json index 4e398a6681..2c7a889ad3 100644 --- a/webapp/packages/plugin-browser/package.json +++ b/webapp/packages/plugin-browser/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-browser", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,26 +11,24 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-browser": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-browser": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "typescript": "^5" + } } diff --git a/webapp/packages/plugin-browser/src/LocaleService.ts b/webapp/packages/plugin-browser/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-browser/src/LocaleService.ts +++ b/webapp/packages/plugin-browser/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-browser/src/PluginBrowserBootstrap.ts b/webapp/packages/plugin-browser/src/PluginBrowserBootstrap.ts deleted file mode 100644 index 22b353efc0..0000000000 --- a/webapp/packages/plugin-browser/src/PluginBrowserBootstrap.ts +++ /dev/null @@ -1,25 +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 { ProcessSnackbar } from '@cloudbeaver/core-blocks'; -import { ServiceWorkerService } from '@cloudbeaver/core-browser'; -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; - -@injectable() -export class PluginBrowserBootstrap extends Bootstrap { - constructor(private readonly serviceWorkerService: ServiceWorkerService, private readonly notificationService: NotificationService) { - super(); - } - register(): void | Promise { - this.serviceWorkerService.onUpdate.addHandler(() => { - this.notificationService.processNotification(() => ProcessSnackbar, {}, { title: 'plugin_browser_update_dialog_title' }); - }); - } - - load(): void | Promise {} -} diff --git a/webapp/packages/plugin-browser/src/PluginBrowserPreloadingBootstrap.ts b/webapp/packages/plugin-browser/src/PluginBrowserPreloadingBootstrap.ts new file mode 100644 index 0000000000..7163143c9d --- /dev/null +++ b/webapp/packages/plugin-browser/src/PluginBrowserPreloadingBootstrap.ts @@ -0,0 +1,30 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ServiceWorkerService } from '@cloudbeaver/core-browser'; +import { displayUpdateStatus, injectable, IPreloadService } from '@cloudbeaver/core-di'; + +@injectable(() => [ServiceWorkerService]) +export class PluginBrowserPreloadingBootstrap implements IPreloadService { + constructor(private readonly serviceWorkerService: ServiceWorkerService) {} + register(): void { + this.serviceWorkerService.onUpdate.addHandler(({ type, progress }) => { + progress = progress || 0; + + switch (type) { + case 'installing': + displayUpdateStatus(progress, 'Installing...'); + break; + case 'updating': + displayUpdateStatus(progress, 'Updating...'); + break; + case 'finished': + break; + } + }); + } +} diff --git a/webapp/packages/plugin-browser/src/index.ts b/webapp/packages/plugin-browser/src/index.ts index eea6e3db82..2e868acf8f 100644 --- a/webapp/packages/plugin-browser/src/index.ts +++ b/webapp/packages/plugin-browser/src/index.ts @@ -1,4 +1,13 @@ -import { browserPlugin } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { browserPlugin } from './manifest.js'; export default browserPlugin; export { browserPlugin }; diff --git a/webapp/packages/plugin-browser/src/locales/en.ts b/webapp/packages/plugin-browser/src/locales/en.ts index 55296a1ec3..d6d1738de6 100644 --- a/webapp/packages/plugin-browser/src/locales/en.ts +++ b/webapp/packages/plugin-browser/src/locales/en.ts @@ -1,4 +1 @@ -export default [ - ['plugin_browser_update_dialog_title', 'Application is updating...'], - ['plugin_browser_update_dialog_message', 'New version of CloudBeaver is available. Do you want to update?'], -]; +export default []; diff --git a/webapp/packages/plugin-browser/src/locales/fr.ts b/webapp/packages/plugin-browser/src/locales/fr.ts new file mode 100644 index 0000000000..d6d1738de6 --- /dev/null +++ b/webapp/packages/plugin-browser/src/locales/fr.ts @@ -0,0 +1 @@ +export default []; diff --git a/webapp/packages/plugin-browser/src/locales/ru.ts b/webapp/packages/plugin-browser/src/locales/ru.ts index 0fe6a0a853..d6d1738de6 100644 --- a/webapp/packages/plugin-browser/src/locales/ru.ts +++ b/webapp/packages/plugin-browser/src/locales/ru.ts @@ -1,4 +1 @@ -export default [ - ['plugin_browser_update_dialog_title', 'Приложение обновляется...'], - ['plugin_browser_update_dialog_message', 'Новая версия CloudBeaver доступна. Хотите обновить?'], -]; +export default []; diff --git a/webapp/packages/plugin-browser/src/locales/vi.ts b/webapp/packages/plugin-browser/src/locales/vi.ts new file mode 100644 index 0000000000..d6d1738de6 --- /dev/null +++ b/webapp/packages/plugin-browser/src/locales/vi.ts @@ -0,0 +1 @@ +export default []; diff --git a/webapp/packages/plugin-browser/src/manifest.ts b/webapp/packages/plugin-browser/src/manifest.ts index ea2fa587c0..6adb5f23b9 100644 --- a/webapp/packages/plugin-browser/src/manifest.ts +++ b/webapp/packages/plugin-browser/src/manifest.ts @@ -1,16 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { LocaleService } from './LocaleService'; -import { PluginBrowserBootstrap } from './PluginBrowserBootstrap'; - export const browserPlugin: PluginManifest = { info: { name: 'Browser plugin' }, - providers: [PluginBrowserBootstrap, LocaleService], }; diff --git a/webapp/packages/plugin-browser/src/module.ts b/webapp/packages/plugin-browser/src/module.ts new file mode 100644 index 0000000000..fea5b4bf04 --- /dev/null +++ b/webapp/packages/plugin-browser/src/module.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, IPreloadService, ModuleRegistry } from '@cloudbeaver/core-di'; +import { PluginBrowserPreloadingBootstrap } from './PluginBrowserPreloadingBootstrap.js'; +import { LocaleService } from './LocaleService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-browser', + + configure: serviceCollection => { + serviceCollection.addSingleton(Bootstrap, LocaleService).addSingleton(IPreloadService, PluginBrowserPreloadingBootstrap); + }, +}); diff --git a/webapp/packages/plugin-browser/tsconfig.json b/webapp/packages/plugin-browser/tsconfig.json index 0fc5cb58fb..579ee4442b 100644 --- a/webapp/packages/plugin-browser/tsconfig.json +++ b/webapp/packages/plugin-browser/tsconfig.json @@ -1,25 +1,23 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../core-blocks/tsconfig.json" + "path": "../core-browser" }, { - "path": "../core-browser/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-events/tsconfig.json" - }, - { - "path": "../core-localization/tsconfig.json" + "path": "../core-localization" } ], "include": [ @@ -31,7 +29,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-codemirror6/.gitignore b/webapp/packages/plugin-codemirror6/.gitignore index 72ea21f191..0975e031bf 100644 --- a/webapp/packages/plugin-codemirror6/.gitignore +++ b/webapp/packages/plugin-codemirror6/.gitignore @@ -5,8 +5,8 @@ /coverage # production -!lib/ -!lib/lib +dist +lib # misc .DS_Store diff --git a/webapp/packages/plugin-codemirror6/package.json b/webapp/packages/plugin-codemirror6/package.json index 04ad3cfe9a..c9ea1223b3 100644 --- a/webapp/packages/plugin-codemirror6/package.json +++ b/webapp/packages/plugin-codemirror6/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-codemirror6", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,38 +11,48 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@codemirror/lang-html": "6.4.6", - "@codemirror/lang-javascript": "6.2.1", - "@codemirror/lang-json": "6.0.1", - "@codemirror/lang-sql": "6.5.4", - "@codemirror/lang-xml": "6.0.2", - "@codemirror/merge": "6.1.2", - "@codemirror/commands": "6.2.5", - "@codemirror/autocomplete": "6.9.0", - "@codemirror/search": "6.5.2", - "@codemirror/language": "6.9.0", - "@codemirror/state": "6.2.1", - "@codemirror/view": "6.18.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "react-dom": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x", - "@testing-library/jest-dom": "~6.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-theming": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@codemirror/autocomplete": "^6", + "@codemirror/commands": "^6", + "@codemirror/lang-html": "^6", + "@codemirror/lang-javascript": "^6", + "@codemirror/lang-json": "^6", + "@codemirror/lang-sql": "^6", + "@codemirror/lang-xml": "^6", + "@codemirror/language": "^6", + "@codemirror/merge": "^6", + "@codemirror/search": "^6", + "@codemirror/state": "^6", + "@codemirror/view": "^6", + "@dbeaver/ui-kit": "workspace:^", + "@lezer/common": "^1", + "@lezer/highlight": "^1", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-codemirror6/src/Editor.tsx b/webapp/packages/plugin-codemirror6/src/Editor.tsx index 10be8ade09..3020e6287d 100644 --- a/webapp/packages/plugin-codemirror6/src/Editor.tsx +++ b/webapp/packages/plugin-codemirror6/src/Editor.tsx @@ -1,22 +1,21 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { forwardRef } from 'react'; -import styled from 'reshadow'; -import { clsx } from '@cloudbeaver/core-utils'; +import { s, useS } from '@cloudbeaver/core-blocks'; -import type { IEditorProps } from './IEditorProps'; -import type { IEditorRef } from './IEditorRef'; -import { ReactCodemirror } from './ReactCodemirror'; -import { EDITOR_BASE_STYLES } from './theme'; -import { useCodemirrorExtensions } from './useCodemirrorExtensions'; -import { type IDefaultExtensions, useEditorDefaultExtensions } from './useEditorDefaultExtensions'; +import type { IEditorProps } from './IEditorProps.js'; +import type { IEditorRef } from './IEditorRef.js'; +import { ReactCodemirror } from './ReactCodemirror.js'; +import { EDITOR_BASE_STYLES } from './theme/index.js'; +import { useCodemirrorExtensions } from './useCodemirrorExtensions.js'; +import { type IDefaultExtensions, useEditorDefaultExtensions } from './useEditorDefaultExtensions.js'; export const Editor = observer( forwardRef(function Editor( @@ -37,13 +36,14 @@ export const Editor = observer( rectangularSelection, keymap, lineWrapping, + search, ...rest }, ref, ) { + useS(EDITOR_BASE_STYLES); extensions = useCodemirrorExtensions(extensions); - const defaultExtensions = useEditorDefaultExtensions({ lineNumbers, tooltips, @@ -60,14 +60,16 @@ export const Editor = observer( rectangularSelection, keymap, lineWrapping, + search, }); extensions.set(...defaultExtensions); - return styled(EDITOR_BASE_STYLES)( - + return ( + // all styles is global scoped so we can't get them from module +
- , +
); }), ); diff --git a/webapp/packages/plugin-codemirror6/src/EditorLoader.ts b/webapp/packages/plugin-codemirror6/src/EditorLoader.ts new file mode 100644 index 0000000000..dbede18f46 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/EditorLoader.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const EditorLoader = importLazyComponent(() => import('./Editor.js').then(m => m.Editor)); diff --git a/webapp/packages/plugin-codemirror6/src/EditorLoader.tsx b/webapp/packages/plugin-codemirror6/src/EditorLoader.tsx deleted file mode 100644 index 5ca1d4cff2..0000000000 --- a/webapp/packages/plugin-codemirror6/src/EditorLoader.tsx +++ /dev/null @@ -1,23 +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 { forwardRef } from 'react'; - -import { ComplexLoader, createComplexLoader } from '@cloudbeaver/core-blocks'; - -import type { IEditorProps } from './IEditorProps'; -import type { IEditorRef } from './IEditorRef'; -import type { IDefaultExtensions } from './useEditorDefaultExtensions'; - -const loader = createComplexLoader(async function loader() { - const { Editor } = await import('./Editor'); - return { Editor }; -}); - -export const EditorLoader = forwardRef(function EditorLoader(props, ref) { - return {({ Editor }) => }; -}); diff --git a/webapp/packages/plugin-codemirror6/src/Hyperlink/Hyperlink.ts b/webapp/packages/plugin-codemirror6/src/Hyperlink/Hyperlink.ts new file mode 100644 index 0000000000..e626d64ba8 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/Hyperlink/Hyperlink.ts @@ -0,0 +1,37 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { RangeSet, RangeValue } from '@codemirror/state'; + +import type { IHyperlinkInfo } from './IHyperlinkInfo.js'; + +export const enum HyperlinkState { + Inactive, + Pending, + Result, +} + +export type HyperlinkSet = RangeSet; + +export class Hyperlink extends RangeValue { + hyperlink: IHyperlinkInfo | null; + state: HyperlinkState; + + constructor() { + super(); + this.hyperlink = null; + this.state = HyperlinkState.Inactive; + } + + static create() { + return new Hyperlink(); + } + + static get none(): HyperlinkSet { + return RangeSet.of([]); + } +} diff --git a/webapp/packages/plugin-codemirror6/src/Hyperlink/HyperlinkLoader.ts b/webapp/packages/plugin-codemirror6/src/Hyperlink/HyperlinkLoader.ts new file mode 100644 index 0000000000..11a61d2573 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/Hyperlink/HyperlinkLoader.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { SelectionRange } from '@codemirror/state'; + +import type { IHyperlinkInfo } from './IHyperlinkInfo.js'; + +export type HyperlinkLoader = (pos: SelectionRange) => Promise; diff --git a/webapp/packages/plugin-codemirror6/src/Hyperlink/IHyperlinkInfo.ts b/webapp/packages/plugin-codemirror6/src/Hyperlink/IHyperlinkInfo.ts new file mode 100644 index 0000000000..6ba2d2776d --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/Hyperlink/IHyperlinkInfo.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export interface IHyperlinkInfo { + tooltip?: string; + onClick: () => void; +} diff --git a/webapp/packages/plugin-codemirror6/src/Hyperlink/useHyperlink.ts b/webapp/packages/plugin-codemirror6/src/Hyperlink/useHyperlink.ts new file mode 100644 index 0000000000..50a3b5fd1b --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/Hyperlink/useHyperlink.ts @@ -0,0 +1,238 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { combineConfig, Compartment, EditorState, type Extension, Facet, Range, SelectionRange, StateEffect, StateField } from '@codemirror/state'; +import { Decoration, EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view'; + +import { Hyperlink, HyperlinkState } from './Hyperlink.js'; +import type { HyperlinkLoader } from './HyperlinkLoader.js'; +import type { IHyperlinkInfo } from './IHyperlinkInfo.js'; + +export interface HyperlinksConfig { + loadLinkInfo: HyperlinkLoader; +} + +export const hyperlinksConfig = Facet.define>({ + combine(configs) { + return combineConfig>(configs, { + loadLinkInfo: () => Promise.resolve(null), + }); + }, +}); + +const metaKeyStateEffect = StateEffect.define(); +const hoverPositionEffect = StateEffect.define(); +const addHyperlinkEffect = StateEffect.define>(); +const updateHyperlinkEffect = StateEffect.define<[Hyperlink, IHyperlinkInfo | null]>(); + +const modKeyState = StateField.define({ + create: () => false, + update(value, tr) { + for (const effect of tr.effects) { + if (effect.is(metaKeyStateEffect)) { + return effect.value; + } + } + return value; + }, +}); + +const hoverPositionState = StateField.define({ + create: () => null, + update(value, tr) { + for (const effect of tr.effects) { + if (effect.is(hoverPositionEffect)) { + return effect.value; + } + } + return value; + }, +}); + +const linksState = StateField.define({ + create: () => Hyperlink.none, + update(hyperlinks, tr) { + hyperlinks = hyperlinks.map(tr.changes); + const isMetaKey = tr.state.field(modKeyState); + + if (!isMetaKey) { + return hyperlinks.size === 0 ? hyperlinks : Hyperlink.none; + } + + for (const effect of tr.effects) { + if (effect.is(addHyperlinkEffect)) { + return hyperlinks.update({ + add: [effect.value], + }); + } + + if (effect.is(updateHyperlinkEffect)) { + return hyperlinks.update({ + filter: (from, to, hyperlink) => { + if (hyperlink === effect.value[0]) { + hyperlink.state = HyperlinkState.Result; + hyperlink.hyperlink = effect.value[1]; + } + return true; + }, + }); + } + } + + return hyperlinks; + }, + provide: linksState => + EditorView.decorations.compute([linksState, hoverPositionState], state => { + const hoverCursor = state.field(hoverPositionState); + const hyperlinks = state.field(linksState).update({ filter: (from, to) => !!hoverCursor && from <= hoverCursor && hoverCursor <= to }); + + const iter = hyperlinks.iter(); + const decorations = []; + + while (iter.value) { + const hyperlink = iter.value; + const link = hyperlink.hyperlink; + + if (link) { + decorations.push( + Decoration.mark({ + class: 'cm-link cm-link-loaded', + attributes: { title: link.tooltip || '' }, + link, + }).range(iter.from, iter.to), + ); + } else { + decorations.push( + Decoration.mark({ + class: 'cm-link cm-link-loading', + }).range(iter.from, iter.to), + ); + } + + iter.next(); + } + + return Decoration.set(decorations); + }), +}); + +const hyperlinkPlugin = ViewPlugin.fromClass( + class { + private pendingUpdate: ReturnType | null = null; + constructor(public view: EditorView) {} + + update(update: ViewUpdate) { + const isMetaKey = update.state.field(modKeyState); + + if (isMetaKey) { + this.scheduleLinkInfoLoad(); + } else { + this.resetLinkInfoLoad(); + } + } + + private scheduleLinkInfoLoad() { + this.resetLinkInfoLoad(); + + this.pendingUpdate = setTimeout(() => { + this.pendingUpdate = null; + + const position = this.getHoveredWordPosition(this.view.state); + + if (position === null) { + return; + } + + const links = this.view.state.field(linksState); + const config = this.view.state.facet(hyperlinksConfig); + + let hyperlink: Hyperlink | null = null; + links.between(position.from, position.to, (from, to, h) => { + if (from === position.from && to === position.to) { + hyperlink = h; + } + }); + + if (!hyperlink) { + hyperlink = Hyperlink.create(); + this.view.dispatch({ effects: addHyperlinkEffect.of(hyperlink!.range(position.from, position.to)) }); + } + + if (hyperlink.state === HyperlinkState.Inactive) { + hyperlink.state = HyperlinkState.Pending; + + config + .loadLinkInfo(position) + .then(link => { + this.view.dispatch({ effects: updateHyperlinkEffect.of([hyperlink!, link]) }); + }) + .catch(() => { + this.view.dispatch({ effects: updateHyperlinkEffect.of([hyperlink!, null]) }); + }); + } + }, 100); + } + + private resetLinkInfoLoad() { + if (this.pendingUpdate) { + clearTimeout(this.pendingUpdate); + this.pendingUpdate = null; + } + } + + private getHoveredWordPosition(state: EditorState): SelectionRange | null { + const position = state.field(hoverPositionState); + + if (position === null) { + return null; + } + + return state.wordAt(position); + } + }, + { + eventHandlers: { + keydown: (event, view) => { + if (event.key === 'Meta') { + view.dispatch({ effects: [metaKeyStateEffect.of(true)] }); + } + }, + keyup: (event, view) => { + if (event.key === 'Meta') { + view.dispatch({ effects: metaKeyStateEffect.of(false) }); + } + }, + mousedown(event, view) { + try { + const linksSet = view.state.field(linksState); + const target = event.target as HTMLElement; + const pos = view.posAtDOM(target); + + const iterator = linksSet.iter(pos); + const hyperlink = iterator.value; + + if (hyperlink) { + const link = hyperlink.hyperlink; + + if (link) { + link.onClick(); + } + } + } catch {} + }, + mousemove: (event, view) => { + const pos = view.posAtCoords({ x: event.clientX, y: event.clientY }); + view.dispatch({ effects: [hoverPositionEffect.of(pos), metaKeyStateEffect.of(event.metaKey)] }); + }, + }, + }, +); + +const EDITOR_AUTOCOMPLETION_COMPARTMENT = new Compartment(); +export function useHyperlink(config: HyperlinksConfig): [Compartment, Extension] { + return [EDITOR_AUTOCOMPLETION_COMPARTMENT, [hyperlinkPlugin, modKeyState, hoverPositionState, linksState, hyperlinksConfig.of(config)]]; +} diff --git a/webapp/packages/plugin-codemirror6/src/IEditorProps.ts b/webapp/packages/plugin-codemirror6/src/IEditorProps.ts index 03b38d14b9..7bf59f99fe 100644 --- a/webapp/packages/plugin-codemirror6/src/IEditorProps.ts +++ b/webapp/packages/plugin-codemirror6/src/IEditorProps.ts @@ -1,11 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IReactCodeMirrorProps } from './IReactCodemirrorProps'; +import type { IReactCodeMirrorProps } from './IReactCodemirrorProps.js'; export interface IEditorProps extends IReactCodeMirrorProps { className?: string; diff --git a/webapp/packages/plugin-codemirror6/src/IEditorRef.ts b/webapp/packages/plugin-codemirror6/src/IEditorRef.ts index 4b9a54938e..dc8d72f870 100644 --- a/webapp/packages/plugin-codemirror6/src/IEditorRef.ts +++ b/webapp/packages/plugin-codemirror6/src/IEditorRef.ts @@ -1,15 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { EditorView } from '@codemirror/view'; -import type { SelectionRange } from '@codemirror/state'; export interface IEditorRef { container: HTMLDivElement | null; view: EditorView | null; - selection: SelectionRange | null; } diff --git a/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts b/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts index 122a4c1251..bec9a880f9 100644 --- a/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts +++ b/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts @@ -1,21 +1,30 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { Compartment, Extension } from '@codemirror/state'; +import type { Compartment, Extension, SelectionRange } from '@codemirror/state'; import type { ViewUpdate } from '@codemirror/view'; +/** Currently we support only main selection range */ +export interface ISelection { + anchor: number; + head?: number; +} + export interface IReactCodeMirrorProps extends React.PropsWithChildren { /** in case of using editor in editing mode its better for performance to use getValue instead */ value?: string; + cursor?: ISelection; incomingValue?: string; getValue?: () => string; extensions?: Map; readonly?: boolean; + copyEventHandler?: (event: ClipboardEvent) => boolean; autoFocus?: boolean; - onChange?: (value: string, update: ViewUpdate) => void; + onChange?: (value: string, selection: SelectionRange, update: ViewUpdate) => void; + onCursorChange?: (selection: SelectionRange, update: ViewUpdate) => void; onUpdate?: (update: ViewUpdate) => void; } diff --git a/webapp/packages/plugin-codemirror6/src/LocaleService.ts b/webapp/packages/plugin-codemirror6/src/LocaleService.ts new file mode 100644 index 0000000000..58ef49c9f0 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/LocaleService.ts @@ -0,0 +1,39 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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(() => [LocalizationService]) +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + override register(): void { + this.localizationService.addProvider(this.provider.bind(this)); + } + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru.js')).default; + case 'it': + return (await import('./locales/it.js')).default; + case 'zh': + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'de': + return (await import('./locales/de.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; + default: + return (await import('./locales/en.js')).default; + } + } +} diff --git a/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx b/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx index f511c9e0e8..e5cd6ffd51 100644 --- a/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx +++ b/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx @@ -1,53 +1,84 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { MergeView } from '@codemirror/merge'; -import { Annotation, Compartment, Extension, StateEffect } from '@codemirror/state'; +import { Annotation, Compartment, EditorState, type Extension, StateEffect, type TransactionSpec } from '@codemirror/state'; import { EditorView, ViewUpdate } from '@codemirror/view'; import { observer } from 'mobx-react-lite'; import { forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useObjectRef } from '@cloudbeaver/core-blocks'; -import type { IEditorRef } from './IEditorRef'; -import type { IReactCodeMirrorProps } from './IReactCodemirrorProps'; -import { type IReactCodemirrorContext, ReactCodemirrorContext } from './ReactCodemirrorContext'; -import { useCodemirrorExtensions } from './useCodemirrorExtensions'; +import type { IEditorRef } from './IEditorRef.js'; +import type { IReactCodeMirrorProps } from './IReactCodemirrorProps.js'; +import { type IReactCodemirrorContext, ReactCodemirrorContext } from './ReactCodemirrorContext.js'; +import { useCodemirrorExtensions } from './useCodemirrorExtensions.js'; +import { validateCursorBoundaries } from './validateCursorBoundaries.js'; +import { ReactCodemirrorSearchPanel } from './ReactCodemirrorSearchPanel.js'; +import { hasInsertProperty } from './hasInsertProperty.js'; const External = Annotation.define(); export const ReactCodemirror = observer( forwardRef(function ReactCodemirror( - { children, getValue, value, incomingValue, extensions = new Map(), readonly, autoFocus, onChange, onUpdate }, + { + children, + getValue, + value, + cursor, + incomingValue, + extensions = new Map(), + readonly, + copyEventHandler, + autoFocus, + onChange, + onCursorChange, + onUpdate, + }, ref, ) { value = value ?? getValue?.(); + const isInitialScriptOpening = useRef(true); const currentExtensions = useRef>(new Map()); - const readOnlyFacet = useMemo(() => EditorView.editable.of(!readonly), [readonly]); - extensions = useCodemirrorExtensions(extensions, readOnlyFacet); + const readOnlyFacet = useMemo(() => { + if (readonly) { + return [EditorState.readOnly.of(true), EditorView.editable.of(false), EditorView.contentAttributes.of({ tabIndex: '0' })]; + } + + return []; + }, [readonly]); + const eventHandlers = useMemo( + () => + EditorView.domEventHandlers({ + copy: copyEventHandler, + }), + [copyEventHandler], + ); + extensions = useCodemirrorExtensions(extensions, [readOnlyFacet, eventHandlers]); const [container, setContainer] = useState(null); const [view, setView] = useState(null); const [incomingView, setIncomingView] = useState(null); - const callbackRef = useObjectRef({ onChange, onUpdate }); - const [selection, setSelection] = useState(view?.state.selection.main ?? null); + const callbackRef = useObjectRef({ onChange, onCursorChange, onUpdate }); useLayoutEffect(() => { if (container) { const updateListener = EditorView.updateListener.of((update: ViewUpdate) => { const remote = update.transactions.some(tr => tr.annotation(External)); - if (update.selectionSet) { - setSelection(update.state.selection.main); - } + const selection = update.state.selection.main; if (update.docChanged && !remote) { const doc = update.state.doc; const value = doc.toString(); - callbackRef.onChange?.(value, update); + callbackRef.onChange?.(value, selection, update); + } + + if (update.selectionSet && !remote) { + callbackRef.onCursorChange?.(selection, update); } callbackRef.onUpdate?.(update); @@ -62,9 +93,17 @@ export const ReactCodemirror = observer( effects.push(compartment.of(extension)); } + const tempState = EditorState.create({ + doc: value, + }); + + const validatedCursor = cursor ? validateCursorBoundaries(cursor, tempState.doc.length) : undefined; + if (incomingValue !== undefined) { merge = new MergeView({ a: { + doc: value, + selection: validatedCursor, extensions: [updateListener, ...effects], }, b: { @@ -77,11 +116,23 @@ export const ReactCodemirror = observer( incomingView = merge.b; } else { editorView = new EditorView({ + state: EditorState.create({ + doc: value, + selection: validatedCursor, + extensions: [updateListener, ...effects], + }), parent: container, - extensions: [updateListener, ...effects], }); } + if (validatedCursor) { + if (validatedCursor.anchor > 0 || validatedCursor.head !== validatedCursor.anchor) { + editorView.dispatch({ + scrollIntoView: true, + }); + } + } + if (incomingView) { setIncomingView(incomingView); } @@ -90,6 +141,12 @@ export const ReactCodemirror = observer( editorView.dom.addEventListener('keydown', event => { const newEvent = new KeyboardEvent('keydown', event); + + // we handle undo/redo on the CaptureView level + if ((newEvent.metaKey || newEvent.ctrlKey) && ['z', 'y'].includes(newEvent.key.toLowerCase())) { + return; + } + document.dispatchEvent(newEvent); }); @@ -136,19 +193,55 @@ export const ReactCodemirror = observer( }); useLayoutEffect(() => { - if (value !== undefined && view && value !== view.state.doc.toString()) { - view.dispatch({ - changes: { from: 0, to: view.state.doc.length, insert: value }, - annotations: [External.of(true)], - }); + if (view) { + const transaction: TransactionSpec = { annotations: [External.of(true)] }; + + let isCursorInDoc = cursor && cursor.anchor > 0 && cursor.anchor < view.state.doc.length; + + if (value !== undefined) { + const newText = view.state.toText(value); + + if (!newText.eq(view.state.doc)) { + transaction.changes = { from: 0, to: view.state.doc.length, insert: newText }; + isCursorInDoc = cursor && cursor.anchor > 0 && cursor.anchor < newText.length; + } + } + + if (cursor) { + const changed = view.state.selection.main.anchor !== cursor.anchor || view.state.selection.main.head !== cursor.head; + + if (changed && isCursorInDoc) { + transaction.selection = cursor; + } + } + + if (hasInsertProperty(transaction.changes) && !transaction.selection && !isInitialScriptOpening.current) { + transaction.selection = { + anchor: transaction.changes.insert?.length ?? 0, + head: transaction.changes.insert?.length ?? 0, + }; + } + + if (transaction.changes) { + view.dispatch({ changes: transaction.changes }); + isInitialScriptOpening.current = false; + } + + if (transaction.selection) { + view.dispatch({ selection: transaction.selection }); + } } - }, [value, view]); + }); useLayoutEffect(() => { - if (incomingValue !== undefined && incomingView && incomingValue !== incomingView.state.doc.toString()) { - incomingView.dispatch({ - changes: { from: 0, to: incomingView.state.doc.length, insert: incomingValue }, - }); + if (incomingValue !== undefined && incomingView) { + const newValue = incomingView.state.toText(incomingValue); + + if (!newValue.eq(incomingView.state.doc)) { + incomingView.dispatch({ + changes: { from: 0, to: incomingView.state.doc.length, insert: newValue }, + }); + } } }, [incomingValue, incomingView]); @@ -163,9 +256,8 @@ export const ReactCodemirror = observer( () => ({ container, view, - selection, }), - [container, view, selection], + [container, view], ); const context = useMemo( @@ -179,6 +271,7 @@ export const ReactCodemirror = observer( return (
+ {children}
diff --git a/webapp/packages/plugin-codemirror6/src/ReactCodemirrorContext.ts b/webapp/packages/plugin-codemirror6/src/ReactCodemirrorContext.ts index 26eae8d465..a26683c358 100644 --- a/webapp/packages/plugin-codemirror6/src/ReactCodemirrorContext.ts +++ b/webapp/packages/plugin-codemirror6/src/ReactCodemirrorContext.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-codemirror6/src/ReactCodemirrorPanel.tsx b/webapp/packages/plugin-codemirror6/src/ReactCodemirrorPanel.tsx index 7698fe031c..9dc6998f5a 100644 --- a/webapp/packages/plugin-codemirror6/src/ReactCodemirrorPanel.tsx +++ b/webapp/packages/plugin-codemirror6/src/ReactCodemirrorPanel.tsx @@ -1,16 +1,17 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { Compartment, StateEffect } from '@codemirror/state'; import { showPanel } from '@codemirror/view'; +import { observer } from 'mobx-react-lite'; import { useContext, useLayoutEffect, useMemo } from 'react'; import { createPortal } from 'react-dom'; -import { ReactCodemirrorContext } from './ReactCodemirrorContext'; +import { ReactCodemirrorContext } from './ReactCodemirrorContext.js'; interface Props extends React.PropsWithChildren { className?: string; @@ -18,7 +19,7 @@ interface Props extends React.PropsWithChildren { top?: boolean; } -export const ReactCodemirrorPanel: React.FC = function ReactCodemirrorPanel({ className, children, incomingView, top }) { +export const ReactCodemirrorPanel: React.FC = observer(function ReactCodemirrorPanel({ className, children, incomingView, top }) { const dom = useMemo(() => document.createElement('div'), []); const compartment = useMemo(() => new Compartment(), []); const context = useContext(ReactCodemirrorContext); @@ -52,5 +53,5 @@ export const ReactCodemirrorPanel: React.FC = function ReactCodemirrorPan return undefined; }, [className]); - return createPortal(children, dom); -}; + return createPortal(children, dom) as any; +}); diff --git a/webapp/packages/plugin-codemirror6/src/ReactCodemirrorSearchPanel.tsx b/webapp/packages/plugin-codemirror6/src/ReactCodemirrorSearchPanel.tsx new file mode 100644 index 0000000000..e5c1e12cef --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/ReactCodemirrorSearchPanel.tsx @@ -0,0 +1,203 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Compartment, EditorState, StateEffect } from '@codemirror/state'; +import { + closeSearchPanel, + findNext, + findPrevious, + getSearchQuery, + replaceAll, + replaceNext, + search, + SearchQuery, + setSearchQuery, + RegExpCursor, +} from '@codemirror/search'; +import { observer } from 'mobx-react-lite'; +import { useContext, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; + +import { ReactCodemirrorContext } from './ReactCodemirrorContext.js'; +import { SearchPanel, type SearchPanelRef } from './SearchPanel/SearchPanel.js'; + +interface Props extends React.PropsWithChildren { + className?: string; + incomingView?: boolean; + top?: boolean; +} + +function getSearchMatchesCount(state: EditorState, config?: SearchQuery) { + const searchQuery = new SearchQuery(config ?? getSearchQuery(state)); + + let cursor; + const counter = { count: 0, current: 1 }; + const options = { ignoreCase: !config?.caseSensitive }; + + if (config?.regexp) { + try { + cursor = new RegExpCursor(state.doc, config.search, options); + } catch (error) { + return counter; + } + } else { + cursor = searchQuery.getCursor(state); + } + + const { from, to } = state.selection.main; + + let item = cursor.next(); + while (!item.done) { + if (item.value.from === from && item.value.to === to) { + counter.current = counter.count + 1; + } + + item = cursor.next(); + counter.count++; + } + + return counter; +} + +export const ReactCodemirrorSearchPanel: React.FC = observer(function ReactCodemirrorSearchPanel({ className, incomingView, top }) { + const dom = useMemo(() => document.createElement('div'), []); + const compartment = useMemo(() => new Compartment(), []); + const context = useContext(ReactCodemirrorContext); + const view = incomingView ? context?.incomingView : context?.view; + const searchPanelRef = useRef(null); + const [searchMatchesCount, setSearchMatchesCount] = useState({ count: 0, current: 1 }); + const [queryState, setQueryState] = useState(() => (view ? getSearchQuery(view?.state) : new SearchQuery({ search: '' }))); + + function updateQuery(updates: Partial) { + if (view) { + view.dispatch({ + effects: setSearchQuery.of(new SearchQuery({ ...queryState, ...updates })), + }); + } + } + + function handleQueryChange(value: string) { + updateQuery({ search: value }); + } + + function handleCaseSensitiveToggle() { + updateQuery({ caseSensitive: !queryState.caseSensitive }); + } + + function handleRegexToggle() { + updateQuery({ regexp: !queryState.regexp }); + } + + function handleWholeWordToggle() { + updateQuery({ wholeWord: !queryState.wholeWord }); + } + + function handleReplaceChange(value: string) { + updateQuery({ replace: value }); + } + + function handleFindNext() { + if (view) { + findNext(view); + } + } + + function handleFindPrevious() { + if (view) { + findPrevious(view); + } + } + + function handleReplaceNext() { + if (view) { + replaceNext(view); + } + } + + function handleReplaceAll() { + if (view) { + replaceAll(view); + } + } + + function handleClose() { + if (view) { + closeSearchPanel(view); + } + } + + useLayoutEffect(() => { + if (view) { + view.dispatch({ + effects: [ + StateEffect.appendConfig.of( + compartment.of( + search({ + createPanel: () => ({ + dom, + top, + update(update) { + const searchQuery = getSearchQuery(update.state); + setQueryState(searchQuery); + setSearchMatchesCount(getSearchMatchesCount(update.state, searchQuery)); + }, + mount: () => { + searchPanelRef.current?.focus(); + }, + }), + }), + ), + ), + ], + }); + + return () => { + view.dispatch({ + effects: compartment.reconfigure([]), + }); + }; + } + + return undefined; + }, [view, top, compartment]); + + useLayoutEffect(() => { + if (className) { + const classes = className.split(' '); + dom.classList.add(...classes); + + return () => { + dom.classList.remove(...classes); + }; + } + return undefined; + }, [className, dom.classList]); + + if (!view) { + return null; + } + + return createPortal( + , + dom, + ); +}); diff --git a/webapp/packages/plugin-codemirror6/src/SearchPanel/SearchPanel.css b/webapp/packages/plugin-codemirror6/src/SearchPanel/SearchPanel.css new file mode 100644 index 0000000000..9600042d7d --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/SearchPanel/SearchPanel.css @@ -0,0 +1,73 @@ +@layer components { + .search-panel { + --dbv-kit-control-outline-offset: -1px; + --dbv-kit-control-outline-width: 1px; + --dbv-kit-icon-btn-outline-offset: 0; + --dbv-kit-input-height: 26px; + --dbv-kit-icon-btn-small-size: 20px; + --dbv-kit-icon-btn-outline-width: 1px; + + display: flex; + align-items: center; + gap: var(--tw-spacing); + + container-type: inline-size; + min-width: 180px; + padding: var(--tw-spacing); + + .dbv-kit-input { + border: 1px solid var(--theme-background); + } + } + + .search-panel__row { + display: flex; + align-items: center; + gap: var(--tw-spacing); + height: 26px; + + &:first-child { + margin-bottom: calc(var(--tw-spacing) / 2); + } + } + + .search-panel__input { + position: relative; + margin-bottom: calc(var(--tw-spacing) / 2); + + .dbv-kit-input { + padding-right: 88px; + } + .dbv-kit-icon-button { + --dbv-kit-icon-btn-primary-background: var(--theme-secondary); + } + } + + .search-panel__inputs { + flex-basis: 420px; + min-width: 120px; + } + + .search-panel__buttons { + flex-basis: 80px; + flex-shrink: 0; + + @container (width >= 280px) { + &:has(.search-panel__matches) { + flex-basis: 140px; + } + } + } + + .search-panel__matches { + display: none; + user-select: none; + font-size: 12px; + flex-shrink: 0; + + @container (width >= 280px) { + display: inline; + padding-inline: var(--tw-spacing); + } + } +} diff --git a/webapp/packages/plugin-codemirror6/src/SearchPanel/SearchPanel.tsx b/webapp/packages/plugin-codemirror6/src/SearchPanel/SearchPanel.tsx new file mode 100644 index 0000000000..e86be0da8a --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/SearchPanel/SearchPanel.tsx @@ -0,0 +1,243 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { SearchQuery } from '@codemirror/search'; +import { observer } from 'mobx-react-lite'; +import { useImperativeHandle, useRef, useState } from 'react'; + +import { Icon, useTranslate } from '@cloudbeaver/core-blocks'; +import { clsx, IconButton, Input, Icon as UIKitIcon } from '@dbeaver/ui-kit'; + +import './SearchPanel.css'; + +export interface SearchPanelRef { + focus: () => void; +} + +interface SearchPanelProps { + isReadOnly: boolean; + searchMatchesCount?: { count: number; current: number }; + queryState: SearchQuery; + ref: React.RefObject; + onQueryChange: (value: string) => void; + onCaseSensitiveToggle: () => void; + onRegexToggle: () => void; + onWholeWordToggle: () => void; + onReplaceChange: (value: string) => void; + onFindNext: () => void; + onFindPrevious: () => void; + onReplaceAll: () => void; + onReplaceNext: () => void; + onClose: () => void; +} + +export const SearchPanel = observer(function SearchPanel({ + isReadOnly, + searchMatchesCount, + queryState, + ref, + onQueryChange, + onCaseSensitiveToggle, + onRegexToggle, + onWholeWordToggle, + onReplaceChange, + onFindNext, + onFindPrevious, + onReplaceAll, + onReplaceNext, + onClose, +}: SearchPanelProps) { + const [showReplace, setShowReplace] = useState(false); + const translate = useTranslate(); + const inputRef = useRef(null); + + useImperativeHandle( + ref, + () => ({ + focus: () => { + inputRef.current?.focus(); + }, + }), + [], + ); + + function handleToggleReplace() { + setShowReplace(prev => !prev); + } + + function handleKeyDown(event: React.KeyboardEvent) { + if (event.key === 'Escape') { + onClose(); + } + } + + function handleInputKeyDown(event: React.KeyboardEvent) { + if (event.key === 'Enter') { + if (event.shiftKey) { + onFindPrevious(); + } else { + onFindNext(); + } + } + } + + function handleReplaceKeyDown(event: React.KeyboardEvent) { + if (event.key === 'Enter') { + if (event.shiftKey) { + onReplaceAll(); + } else { + onReplaceNext(); + } + } + } + + return ( +
+ {!isReadOnly && ( + + + + )} + +
+
+ onQueryChange(event.target.value)} + /> +
+ + + + + + + + + + + +
+
+ {showReplace && ( +
+ onReplaceChange(event.target.value)} + /> +
+ )} +
+
+
+ {queryState.search && searchMatchesCount && ( + + {searchMatchesCount.count > 0 + ? `${searchMatchesCount.current} ${translate('plugin_codemirror_search_matches_of')} ${searchMatchesCount.count}` + : translate('plugin_codemirror_search_matches_none')} + + )} + + + + + + + + + + + + +
+ + {showReplace && ( +
+ + + + + + + +
+ )} +
+
+ ); +}); diff --git a/webapp/packages/plugin-codemirror6/src/hasInsertProperty.ts b/webapp/packages/plugin-codemirror6/src/hasInsertProperty.ts new file mode 100644 index 0000000000..5988b45c91 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/hasInsertProperty.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ChangeSpec, Text } from '@codemirror/state'; + +export function hasInsertProperty(spec: ChangeSpec | undefined): spec is { from: number; to?: number; insert?: string | Text } { + return typeof spec === 'object' && spec !== null && 'insert' in spec; +} \ No newline at end of file diff --git a/webapp/packages/plugin-codemirror6/src/index.ts b/webapp/packages/plugin-codemirror6/src/index.ts index 0eca49d539..64f5e2ce8a 100644 --- a/webapp/packages/plugin-codemirror6/src/index.ts +++ b/webapp/packages/plugin-codemirror6/src/index.ts @@ -1,12 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; import { createComplexLoader } from '@cloudbeaver/core-blocks'; -export * from './ReactCodemirrorPanel'; -export * from './EditorLoader'; -export * from './IEditorProps'; -export * from './IEditorRef'; -export * from './useEditorDefaultExtensions'; -export * from './useEditorAutocompletion'; -export * from './useCodemirrorExtensions'; +export * from './ReactCodemirrorPanel.js'; +export * from './EditorLoader.js'; +export * from './IEditorProps.js'; +export * from './IEditorRef.js'; +export * from './useEditorDefaultExtensions.js'; +export * from './useEditorAutocompletion.js'; +export * from './useCodemirrorExtensions.js'; + +export * from './Hyperlink/HyperlinkLoader.js'; +export * from './Hyperlink/IHyperlinkInfo.js'; +export * from './Hyperlink/useHyperlink.js'; export * from '@codemirror/view'; export * from '@codemirror/state'; @@ -26,3 +39,4 @@ export const MSSQLLoader = createComplexLoader(async () => (await import('@codem export const SQLiteLoader = createComplexLoader(async () => (await import('@codemirror/lang-sql')).SQLite); export const CassandraLoader = createComplexLoader(async () => (await import('@codemirror/lang-sql')).Cassandra); export const PLSQLLoader = createComplexLoader(async () => (await import('@codemirror/lang-sql')).PLSQL); +export { manifest as codemirror6Manifest } from './manifest.js'; diff --git a/webapp/packages/plugin-codemirror6/src/locales/de.ts b/webapp/packages/plugin-codemirror6/src/locales/de.ts new file mode 100644 index 0000000000..885576d654 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/locales/de.ts @@ -0,0 +1,14 @@ +export default [ + ['plugin_codemirror_search_input_placeholder', 'Search...'], + ['plugin_codemirror_search_case_sensitive', 'Case Sensitive'], + ['plugin_codemirror_search_literal', 'Use Regular Expression'], + ['plugin_codemirror_search_whole_word', 'Match Whole Word'], + ['plugin_codemirror_search_replace', 'Replace'], + ['plugin_codemirror_search_replace_toggle', 'Toggle Replace'], + ['plugin_codemirror_search_replace_all', 'Replace All'], + ['plugin_codemirror_search_find_next', 'Find Next (Enter)'], + ['plugin_codemirror_search_find_previous', 'Find Previous (Shift+Enter)'], + ['plugin_codemirror_search_matches_of', 'of'], + ['plugin_codemirror_search_matches_none', 'Not found'], + ['plugin_codemirror_search_close', 'Close (Escape)'], +]; diff --git a/webapp/packages/plugin-codemirror6/src/locales/en.ts b/webapp/packages/plugin-codemirror6/src/locales/en.ts new file mode 100644 index 0000000000..885576d654 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/locales/en.ts @@ -0,0 +1,14 @@ +export default [ + ['plugin_codemirror_search_input_placeholder', 'Search...'], + ['plugin_codemirror_search_case_sensitive', 'Case Sensitive'], + ['plugin_codemirror_search_literal', 'Use Regular Expression'], + ['plugin_codemirror_search_whole_word', 'Match Whole Word'], + ['plugin_codemirror_search_replace', 'Replace'], + ['plugin_codemirror_search_replace_toggle', 'Toggle Replace'], + ['plugin_codemirror_search_replace_all', 'Replace All'], + ['plugin_codemirror_search_find_next', 'Find Next (Enter)'], + ['plugin_codemirror_search_find_previous', 'Find Previous (Shift+Enter)'], + ['plugin_codemirror_search_matches_of', 'of'], + ['plugin_codemirror_search_matches_none', 'Not found'], + ['plugin_codemirror_search_close', 'Close (Escape)'], +]; diff --git a/webapp/packages/plugin-codemirror6/src/locales/fr.ts b/webapp/packages/plugin-codemirror6/src/locales/fr.ts new file mode 100644 index 0000000000..885576d654 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/locales/fr.ts @@ -0,0 +1,14 @@ +export default [ + ['plugin_codemirror_search_input_placeholder', 'Search...'], + ['plugin_codemirror_search_case_sensitive', 'Case Sensitive'], + ['plugin_codemirror_search_literal', 'Use Regular Expression'], + ['plugin_codemirror_search_whole_word', 'Match Whole Word'], + ['plugin_codemirror_search_replace', 'Replace'], + ['plugin_codemirror_search_replace_toggle', 'Toggle Replace'], + ['plugin_codemirror_search_replace_all', 'Replace All'], + ['plugin_codemirror_search_find_next', 'Find Next (Enter)'], + ['plugin_codemirror_search_find_previous', 'Find Previous (Shift+Enter)'], + ['plugin_codemirror_search_matches_of', 'of'], + ['plugin_codemirror_search_matches_none', 'Not found'], + ['plugin_codemirror_search_close', 'Close (Escape)'], +]; diff --git a/webapp/packages/plugin-codemirror6/src/locales/it.ts b/webapp/packages/plugin-codemirror6/src/locales/it.ts new file mode 100644 index 0000000000..885576d654 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/locales/it.ts @@ -0,0 +1,14 @@ +export default [ + ['plugin_codemirror_search_input_placeholder', 'Search...'], + ['plugin_codemirror_search_case_sensitive', 'Case Sensitive'], + ['plugin_codemirror_search_literal', 'Use Regular Expression'], + ['plugin_codemirror_search_whole_word', 'Match Whole Word'], + ['plugin_codemirror_search_replace', 'Replace'], + ['plugin_codemirror_search_replace_toggle', 'Toggle Replace'], + ['plugin_codemirror_search_replace_all', 'Replace All'], + ['plugin_codemirror_search_find_next', 'Find Next (Enter)'], + ['plugin_codemirror_search_find_previous', 'Find Previous (Shift+Enter)'], + ['plugin_codemirror_search_matches_of', 'of'], + ['plugin_codemirror_search_matches_none', 'Not found'], + ['plugin_codemirror_search_close', 'Close (Escape)'], +]; diff --git a/webapp/packages/plugin-codemirror6/src/locales/ru.ts b/webapp/packages/plugin-codemirror6/src/locales/ru.ts new file mode 100644 index 0000000000..2f56b52a2d --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/locales/ru.ts @@ -0,0 +1,14 @@ +export default [ + ['plugin_codemirror_search_input_placeholder', 'Найти...'], + ['plugin_codemirror_search_case_sensitive', 'С учетом регистра'], + ['plugin_codemirror_search_literal', 'Использовать регулярное выражение'], + ['plugin_codemirror_search_whole_word', 'Искать целое слово'], + ['plugin_codemirror_search_replace', 'Заменить'], + ['plugin_codemirror_search_replace_toggle', 'Вкл/выкл. замену'], + ['plugin_codemirror_search_replace_all', 'Заменить все'], + ['plugin_codemirror_search_find_next', 'Найти следующее (Enter)'], + ['plugin_codemirror_search_find_previous', 'Найти предыдущее (Shift+Enter)'], + ['plugin_codemirror_search_matches_of', 'из'], + ['plugin_codemirror_search_matches_none', 'Не найдено'], + ['plugin_codemirror_search_close', 'Закрыть (Escape)'], +]; diff --git a/webapp/packages/plugin-codemirror6/src/locales/vi.ts b/webapp/packages/plugin-codemirror6/src/locales/vi.ts new file mode 100644 index 0000000000..885576d654 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/locales/vi.ts @@ -0,0 +1,14 @@ +export default [ + ['plugin_codemirror_search_input_placeholder', 'Search...'], + ['plugin_codemirror_search_case_sensitive', 'Case Sensitive'], + ['plugin_codemirror_search_literal', 'Use Regular Expression'], + ['plugin_codemirror_search_whole_word', 'Match Whole Word'], + ['plugin_codemirror_search_replace', 'Replace'], + ['plugin_codemirror_search_replace_toggle', 'Toggle Replace'], + ['plugin_codemirror_search_replace_all', 'Replace All'], + ['plugin_codemirror_search_find_next', 'Find Next (Enter)'], + ['plugin_codemirror_search_find_previous', 'Find Previous (Shift+Enter)'], + ['plugin_codemirror_search_matches_of', 'of'], + ['plugin_codemirror_search_matches_none', 'Not found'], + ['plugin_codemirror_search_close', 'Close (Escape)'], +]; diff --git a/webapp/packages/plugin-codemirror6/src/locales/zh.ts b/webapp/packages/plugin-codemirror6/src/locales/zh.ts new file mode 100644 index 0000000000..885576d654 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/locales/zh.ts @@ -0,0 +1,14 @@ +export default [ + ['plugin_codemirror_search_input_placeholder', 'Search...'], + ['plugin_codemirror_search_case_sensitive', 'Case Sensitive'], + ['plugin_codemirror_search_literal', 'Use Regular Expression'], + ['plugin_codemirror_search_whole_word', 'Match Whole Word'], + ['plugin_codemirror_search_replace', 'Replace'], + ['plugin_codemirror_search_replace_toggle', 'Toggle Replace'], + ['plugin_codemirror_search_replace_all', 'Replace All'], + ['plugin_codemirror_search_find_next', 'Find Next (Enter)'], + ['plugin_codemirror_search_find_previous', 'Find Previous (Shift+Enter)'], + ['plugin_codemirror_search_matches_of', 'of'], + ['plugin_codemirror_search_matches_none', 'Not found'], + ['plugin_codemirror_search_close', 'Close (Escape)'], +]; diff --git a/webapp/packages/plugin-codemirror6/src/manifest.ts b/webapp/packages/plugin-codemirror6/src/manifest.ts new file mode 100644 index 0000000000..d208e0fb40 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/manifest.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; + +export const manifest: PluginManifest = { + info: { + name: 'Codemirror 6 Plugin', + }, +}; diff --git a/webapp/packages/plugin-codemirror6/src/module.ts b/webapp/packages/plugin-codemirror6/src/module.ts new file mode 100644 index 0000000000..1a0909f585 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/module.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ModuleRegistry, Bootstrap } from '@cloudbeaver/core-di'; +import { LocaleService } from './LocaleService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-codemirror6', + + configure: serviceCollection => { + serviceCollection.addSingleton(Bootstrap, LocaleService); + }, +}); diff --git a/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-autocompletion.scss b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-autocompletion.scss index c166524be8..b05bff013e 100644 --- a/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-autocompletion.scss +++ b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-autocompletion.scss @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-tooltip.scss b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-tooltip.scss index 2d90004448..45e900d061 100644 --- a/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-tooltip.scss +++ b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-tooltip.scss @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor.scss b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor.scss index 496b57f9c9..01f69ec2be 100644 --- a/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor.scss +++ b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor.scss @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -21,7 +21,8 @@ .ReactCodemirror, .cm-mergeView, .cm-mergeViewEditors, - .cm-editor { + .cm-editor, + .cm-gotoLine { @include mdc-typography('body2'); } @@ -48,6 +49,14 @@ border-left-color: $cursor; } + .editor .cm-link { + &.cm-link-loaded { + @include mdc-theme-prop(color, primary, false); + text-decoration: underline; + cursor: pointer; + } + } + .editor .cm-dropCursor { border-left: 1.2px solid $cursor; } @@ -59,7 +68,8 @@ @include mdc-theme-prop(border-color, background, false); } - .cm-panels-top { + .cm-panels-top, + .cm-panels-bottom { @include mdc-theme-prop(border-color, background, false); } @@ -84,6 +94,75 @@ */ } + .editor .cm-gotoLine.cm-panel { + display: flex; + align-items: flex-end; + flex-wrap: wrap; + padding: 4px 8px; + gap: 8px; + min-width: 160px; + container-type: inline-size; + + & label { + display: flex; + flex-wrap: wrap; + align-items: center; + text-transform: uppercase; + letter-spacing: -0.1px; + gap: 4px; + } + + & input { + @include mdc-typography('body2'); + max-width: 60px; + height: 24px; + padding-inline: 8px; + + &:focus { + @include mdc-theme-prop(border-color, primary, false); + } + } + & .cm-button { + @include mdc-typography('body2'); + background: transparent; + height: 24px; + padding-block: 0; + margin-right: 24px; + + font-weight: 500; + text-transform: uppercase; + + color: var(--theme-primary); + border-radius: 4px; + border-color: var(--theme-primary); + + &:hover { + background: color-mix(in srgb, transparent, var(--theme-primary) 5%); + } + + @container (width < 240px) { + display: none; + } + } + + & button[name='close'] { + margin-left: auto; + padding: 0 8px; + top: 4px; + height: 24px; + width: 24px; + border-radius: 4px; + background: transparent; + color: var(--theme-on-surface); + border-color: var(--theme-on-surface); + font-size: 16px; + + &:hover { + background: color-mix(in srgb, transparent, var(--theme-on-surface) 5%); + } + } + } + .editor .cm-content ::selection { @include mdc-theme-prop(background, primary, false); color: white; @@ -103,7 +182,7 @@ .editor .cm-gutters .cm-foldGutter .cm-gutterElement-icon { padding: 0 3px 0 5px; - width: 12px; + width: 20px; height: 12px; display: flex; @@ -209,4 +288,32 @@ .editor .tok-type { color: $variable-3; } + + .editor .tok-typeName { + color: $type-name; + } + + .editor .tok-punctuation { + color: $punctuation; + } + + .editor .tok-propertyName { + color: $property-name; + } + + .editor .tok-className { + color: $class-name; + } + + .editor .tok-bool { + color: $bool; + } + + .editor .tok-variableName { + color: $variable-name; + } + + .editor .tok-labelName { + color: $label-name; + } } diff --git a/webapp/packages/plugin-codemirror6/src/theme/dark.module.scss b/webapp/packages/plugin-codemirror6/src/theme/dark.module.scss index 921d84044e..d8ce497fb0 100644 --- a/webapp/packages/plugin-codemirror6/src/theme/dark.module.scss +++ b/webapp/packages/plugin-codemirror6/src/theme/dark.module.scss @@ -1,19 +1,19 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -@import "@cloudbeaver/core-theming/src/styles/theme-dark"; -@import "base-code-editor"; -@import "base-code-editor-autocompletion"; -@import "base-code-editor-tooltip"; +@import '@cloudbeaver/core-theming/src/styles/theme-dark'; +@import './base-code-editor'; +@import './base-code-editor-autocompletion'; +@import './base-code-editor-tooltip'; $meta: #ff1717; $keyword: rgb(115, 158, 202); -$atom: #604aff; +$atom: #56c8d8; $number: rgb(192, 192, 192); $def: #00f; $variable: rgb(126, 186, 211); @@ -25,7 +25,7 @@ $comment: rgb(102, 151, 104); $string: rgb(202, 197, 128); $string-2: #f50; $qualifier: #555; -$builtin:#ab87ff; +$builtin: #ab87ff; $bracket: #cc7; $tag: #170; $attribute: #00c; @@ -34,6 +34,13 @@ $error: #f00; $delimiter: rgb(238, 204, 100); $cursor: #fff; $active-query: $color-positive; +$type-name: #c586c0; +$class-name: #c586c0; +$punctuation: #ffffff8c; +$property-name: #569cd6; +$bool: #56c8d8; +$variable-name: #56c8d8; +$label-name: #cf6edf; :global .#{$theme-class} { @include base-code-editor; diff --git a/webapp/packages/plugin-codemirror6/src/theme/index.ts b/webapp/packages/plugin-codemirror6/src/theme/index.ts index 15c73a4500..ccfd82ad64 100644 --- a/webapp/packages/plugin-codemirror6/src/theme/index.ts +++ b/webapp/packages/plugin-codemirror6/src/theme/index.ts @@ -1 +1,8 @@ -export * from './styles'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +export * from './styles.js'; diff --git a/webapp/packages/plugin-codemirror6/src/theme/light.module.scss b/webapp/packages/plugin-codemirror6/src/theme/light.module.scss index 4d28833c25..aad7e81507 100644 --- a/webapp/packages/plugin-codemirror6/src/theme/light.module.scss +++ b/webapp/packages/plugin-codemirror6/src/theme/light.module.scss @@ -1,15 +1,15 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -@import "@cloudbeaver/core-theming/src/styles/theme-light"; -@import "base-code-editor"; -@import "base-code-editor-autocompletion"; -@import "base-code-editor-tooltip"; +@import '@cloudbeaver/core-theming/src/styles/theme-light'; +@import './base-code-editor'; +@import './base-code-editor-autocompletion'; +@import './base-code-editor-tooltip'; $meta: #ff1717; $keyword: #07a; @@ -22,7 +22,7 @@ $variable-3: #a8bd00; $property: black; $operator: #a67f59; $comment: #3f7f5f; -$string: #690; +$string: #1f8900; $string-2: #f50; $qualifier: #555; $builtin: #07a; @@ -34,6 +34,13 @@ $error: #f00; $delimiter: rgb(238, 204, 100); $cursor: #000; $active-query: $color-positive; +$type-name: #007acc; +$class-name: #007acc; +$punctuation: #6d6d6d; +$property-name: #c42626; +$bool: #905; +$variable-name: #905; +$label-name: #81a1c1; :global .#{$theme-class} { @include base-code-editor; diff --git a/webapp/packages/plugin-codemirror6/src/theme/styles.ts b/webapp/packages/plugin-codemirror6/src/theme/styles.ts index c839d4462c..35199acad6 100644 --- a/webapp/packages/plugin-codemirror6/src/theme/styles.ts +++ b/webapp/packages/plugin-codemirror6/src/theme/styles.ts @@ -1,23 +1,26 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { ThemeSelector } from '@cloudbeaver/core-theming'; +import type Dark from './dark.module.scss'; +import type Light from './light.module.scss'; + export const EDITOR_BASE_STYLES: ThemeSelector = async theme => { - let styles: any; + let styles: typeof Light & typeof Dark; switch (theme) { case 'dark': - styles = await import('./dark.module.scss'); + styles = (await import('./dark.module.scss')).default; break; default: - styles = await import('./light.module.scss'); + styles = (await import('./light.module.scss')).default; break; } - return styles.default; + return styles; }; diff --git a/webapp/packages/plugin-codemirror6/src/useCodemirrorExtensions.ts b/webapp/packages/plugin-codemirror6/src/useCodemirrorExtensions.ts index 6593810b54..d41407f16f 100644 --- a/webapp/packages/plugin-codemirror6/src/useCodemirrorExtensions.ts +++ b/webapp/packages/plugin-codemirror6/src/useCodemirrorExtensions.ts @@ -1,11 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { Compartment, Extension } from '@codemirror/state'; +import { Compartment, type Extension } from '@codemirror/state'; import { useState } from 'react'; export function useCodemirrorExtensions(extensions?: Map, staticExtensions?: Extension): Map { diff --git a/webapp/packages/plugin-codemirror6/src/useEditorAutocompletion.ts b/webapp/packages/plugin-codemirror6/src/useEditorAutocompletion.ts index b683416afc..5a3bb47ef3 100644 --- a/webapp/packages/plugin-codemirror6/src/useEditorAutocompletion.ts +++ b/webapp/packages/plugin-codemirror6/src/useEditorAutocompletion.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-codemirror6/src/useEditorDefaultExtensions.ts b/webapp/packages/plugin-codemirror6/src/useEditorDefaultExtensions.ts index 8830f64967..0d611b6135 100644 --- a/webapp/packages/plugin-codemirror6/src/useEditorDefaultExtensions.ts +++ b/webapp/packages/plugin-codemirror6/src/useEditorDefaultExtensions.ts @@ -1,14 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { defaultKeymap, indentWithTab } from '@codemirror/commands'; import { bracketMatching, foldGutter, indentOnInput, syntaxHighlighting } from '@codemirror/language'; -import { highlightSelectionMatches } from '@codemirror/search'; -import { Compartment, Extension } from '@codemirror/state'; +import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'; +import { Compartment, type Extension } from '@codemirror/state'; import { crosshairCursor, dropCursor, @@ -24,7 +24,8 @@ import { import { classHighlighter } from '@lezer/highlight'; import { useRef } from 'react'; -import { clsx, GlobalConstants, isObjectsEqual } from '@cloudbeaver/core-utils'; +import { GlobalConstants, isObjectsEqual } from '@cloudbeaver/core-utils'; +import { clsx } from '@dbeaver/ui-kit'; // @TODO allow to configure bindings outside of the component const DEFAULT_KEY_MAP = defaultKeymap.filter(binding => binding.mac !== 'Ctrl-f' && binding.key !== 'Mod-Enter'); @@ -34,6 +35,7 @@ DEFAULT_KEY_MAP.push({ key: 'Mod-s', run: () => true, }); +DEFAULT_KEY_MAP.push(...searchKeymap); const defaultExtensionsFlags: IDefaultExtensions = { lineNumbers: false, @@ -51,6 +53,7 @@ const defaultExtensionsFlags: IDefaultExtensions = { rectangularSelection: true, keymap: true, lineWrapping: false, + search: true, }; export interface IDefaultExtensions { @@ -69,6 +72,7 @@ export interface IDefaultExtensions { rectangularSelection?: boolean; keymap?: boolean; lineWrapping?: boolean; + search?: boolean; } const extensionMap = { diff --git a/webapp/packages/plugin-codemirror6/src/validateCursorBoundaries.ts b/webapp/packages/plugin-codemirror6/src/validateCursorBoundaries.ts new file mode 100644 index 0000000000..59035ea4ef --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/validateCursorBoundaries.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { ISelection } from './IReactCodemirrorProps.js'; + +export function validateCursorBoundaries(selection: ISelection, documentLength: number): ISelection { + return { + anchor: Math.min(selection.anchor, documentLength), + head: selection.head === undefined ? undefined : Math.min(selection.head, documentLength), + }; +} diff --git a/webapp/packages/plugin-codemirror6/tsconfig.json b/webapp/packages/plugin-codemirror6/tsconfig.json index 4d3b05aaf0..7ef04e01e0 100644 --- a/webapp/packages/plugin-codemirror6/tsconfig.json +++ b/webapp/packages/plugin-codemirror6/tsconfig.json @@ -1,19 +1,32 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../core-blocks/tsconfig.json" + "path": "../../common-react/@dbeaver/ui-kit" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-cli" + }, + { + "path": "../core-di" + }, + { + "path": "../core-localization" + }, + { + "path": "../core-theming" + }, + { + "path": "../core-utils" } ], "include": [ @@ -25,7 +38,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-connection-custom/package.json b/webapp/packages/plugin-connection-custom/package.json index b94d953188..047162c6f9 100644 --- a/webapp/packages/plugin-connection-custom/package.json +++ b/webapp/packages/plugin-connection-custom/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-connection-custom", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,33 +11,44 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-connections": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-projects": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-navigation-tree": "workspace:*", + "@cloudbeaver/core-projects": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-settings": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-connections": "workspace:*", + "@cloudbeaver/plugin-navigation-tabs": "workspace:*", + "@cloudbeaver/plugin-navigation-tree": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-connection-custom/src/Actions/ACTION_CONNECTION_CUSTOM.ts b/webapp/packages/plugin-connection-custom/src/Actions/ACTION_CONNECTION_CUSTOM.ts index 88faafa3ce..a446ff3e2b 100644 --- a/webapp/packages/plugin-connection-custom/src/Actions/ACTION_CONNECTION_CUSTOM.ts +++ b/webapp/packages/plugin-connection-custom/src/Actions/ACTION_CONNECTION_CUSTOM.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,4 +9,5 @@ import { createAction } from '@cloudbeaver/core-view'; export const ACTION_CONNECTION_CUSTOM = createAction('connection-custom', { label: 'plugin_connection_custom_action_custom_label', + tooltip: 'plugin_connection_custom_action_custom_tooltip', }); diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/CustomConnectionController.ts b/webapp/packages/plugin-connection-custom/src/CustomConnection/CustomConnectionController.ts deleted file mode 100644 index 3ad960563f..0000000000 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/CustomConnectionController.ts +++ /dev/null @@ -1,79 +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 { computed, makeObservable, observable } from 'mobx'; - -import { ConnectionsManagerService, DBDriver, DBDriverResource } from '@cloudbeaver/core-connections'; -import { IInitializableController, injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { ProjectsService } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { isArraysEqual } from '@cloudbeaver/core-utils'; -import { PublicConnectionFormService } from '@cloudbeaver/plugin-connections'; - -@injectable() -export class CustomConnectionController implements IInitializableController { - isLoading: boolean; - onClose!: () => void; - - get drivers(): DBDriver[] { - return this.dbDriverResource.enabledDrivers; - } - - constructor( - private readonly dbDriverResource: DBDriverResource, - private readonly notificationService: NotificationService, - private readonly projectsService: ProjectsService, - private readonly publicConnectionFormService: PublicConnectionFormService, - private readonly connectionsManagerService: ConnectionsManagerService, - ) { - this.isLoading = true; - - makeObservable(this, { - isLoading: observable, - drivers: computed({ - equals: isArraysEqual, - }), - }); - } - - init(onClose: () => void): void { - this.loadDBDrivers(); - this.onClose = onClose; - } - - onDriverSelect = async (driverId: string) => { - await this.projectsService.load(); - - const projects = this.connectionsManagerService.createConnectionProjects; - - if (projects.length === 0) { - this.notificationService.logError({ title: 'core_projects_no_default_project' }); - return; - } - - const state = await this.publicConnectionFormService.open( - projects[0].id, - { driverId }, - this.drivers.map(driver => driver.id), - ); - - if (state) { - this.onClose(); - } - }; - - private async loadDBDrivers() { - try { - await this.dbDriverResource.load(CachedMapAllKey); - } catch (exception: any) { - this.notificationService.logException(exception, "Can't load database drivers"); - } finally { - this.isLoading = false; - } - } -} diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/CustomConnectionDialog.tsx b/webapp/packages/plugin-connection-custom/src/CustomConnection/CustomConnectionDialog.tsx deleted file mode 100644 index 7322363994..0000000000 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/CustomConnectionDialog.tsx +++ /dev/null @@ -1,30 +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 { observer } from 'mobx-react-lite'; - -import { useTranslate } from '@cloudbeaver/core-blocks'; -import { useController } from '@cloudbeaver/core-di'; -import type { DialogComponent } from '@cloudbeaver/core-dialogs'; - -import { CustomConnectionController } from './CustomConnectionController'; -import { DriverSelectorDialog } from './DriverSelectorDialog/DriverSelectorDialog'; - -export const CustomConnectionDialog: DialogComponent = observer(function CustomConnectionDialog({ rejectDialog }) { - const controller = useController(CustomConnectionController, rejectDialog); - const translate = useTranslate(); - - return ( - - ); -}); diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.m.css b/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.m.css deleted file mode 100644 index 4835c4cc1d..0000000000 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.m.css +++ /dev/null @@ -1,5 +0,0 @@ -.staticImage { - box-sizing: border-box; - width: 24px; - max-height: 24px; -}; \ No newline at end of file diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.tsx b/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.tsx deleted file mode 100644 index a85a5089f3..0000000000 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/Driver.tsx +++ /dev/null @@ -1,40 +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 { observer } from 'mobx-react-lite'; -import { useCallback } from 'react'; - -import { ListItem, ListItemDescription, ListItemIcon, ListItemName, s, StaticImage, useS } from '@cloudbeaver/core-blocks'; - -import style from './Driver.m.css'; - -export interface IDriver { - id: string; - icon?: string; - name?: string; - description?: string; -} - -interface Props { - driver: IDriver; - onSelect: (driverId: string) => void; -} - -export const Driver = observer(function Driver({ driver, onSelect }) { - const select = useCallback(() => onSelect(driver.id), [driver]); - const styles = useS(style); - - return ( - - - - - {driver.name} - {driver.description} - - ); -}); diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelector.m.css b/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelector.m.css deleted file mode 100644 index 54257352a3..0000000000 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelector.m.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - display: flex; - flex-direction: column; - overflow: auto; -} \ No newline at end of file diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelectorDialog.m.css b/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelectorDialog.m.css deleted file mode 100644 index 6fa529d844..0000000000 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelectorDialog.m.css +++ /dev/null @@ -1,3 +0,0 @@ -.driverSelector { - flex: 1; -} diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelectorDialog.tsx b/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelectorDialog.tsx deleted file mode 100644 index c8d62d124f..0000000000 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelectorDialog.tsx +++ /dev/null @@ -1,36 +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 { observer } from 'mobx-react-lite'; - -import { CommonDialogBody, CommonDialogHeader, CommonDialogWrapper, Loader, s, useS } from '@cloudbeaver/core-blocks'; - -import type { IDriver } from './Driver'; -import { DriverSelector } from './DriverSelector'; -import styles from './DriverSelectorDialog.m.css'; - -interface IProps { - title: string; - drivers: IDriver[]; - isLoading: boolean; - onSelect: (driverId: string) => void; - onClose: () => void; -} - -export const DriverSelectorDialog = observer(function DriverSelectorDialog({ title, drivers, isLoading, onSelect, onClose }) { - const style = useS(styles); - - return ( - - - - {isLoading && } - {!isLoading && } - - - ); -}); diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts b/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts index f28864745a..7572037245 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts +++ b/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts @@ -1,23 +1,39 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { ConnectionsManagerService } from '@cloudbeaver/core-connections'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { ConnectionsManagerService, getFolderPath } from '@cloudbeaver/core-connections'; +import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; -import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { DATA_CONTEXT_NAV_NODE, isConnectionFolder, isProjectNode } from '@cloudbeaver/core-navigation-tree'; +import { getProjectNodeId, ProjectInfoResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; -import { ActionService, MenuService } from '@cloudbeaver/core-view'; -import { MENU_CONNECTIONS } from '@cloudbeaver/plugin-connections'; +import { ActionService, DATA_CONTEXT_MENU, type IAction, MenuService } from '@cloudbeaver/core-view'; +import { ACTION_TREE_CREATE_CONNECTION, MENU_CONNECTIONS, MENU_TREE_CREATE_CONNECTION } from '@cloudbeaver/plugin-connections'; +import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS, TreeSelectionService } from '@cloudbeaver/plugin-navigation-tree'; +import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; -import { ACTION_CONNECTION_CUSTOM } from './Actions/ACTION_CONNECTION_CUSTOM'; -import { CustomConnectionDialog } from './CustomConnection/CustomConnectionDialog'; -import { CustomConnectionSettingsService } from './CustomConnectionSettingsService'; +import { ACTION_CONNECTION_CUSTOM } from './Actions/ACTION_CONNECTION_CUSTOM.js'; +import { CustomConnectionSettingsService } from './CustomConnectionSettingsService.js'; -@injectable() +const DriverSelectorDialog = importLazyComponent(() => import('./DriverSelector/DriverSelectorDialog.js').then(m => m.DriverSelectorDialog)); +const WelcomeNewConnection = importLazyComponent(() => import('./WelcomeNewConnection.js').then(m => m.WelcomeNewConnection)); + +@injectable(() => [ + CommonDialogService, + ProjectInfoResource, + MenuService, + ActionService, + ConnectionsManagerService, + CustomConnectionSettingsService, + TreeSelectionService, + NavigationTabsService, +]) export class CustomConnectionPluginBootstrap extends Bootstrap { constructor( private readonly commonDialogService: CommonDialogService, @@ -26,47 +42,109 @@ export class CustomConnectionPluginBootstrap extends Bootstrap { private readonly actionService: ActionService, private readonly connectionsManagerService: ConnectionsManagerService, private readonly customConnectionSettingsService: CustomConnectionSettingsService, + private readonly treeSelectionService: TreeSelectionService, + private readonly navigationTabsService: NavigationTabsService, ) { super(); } - register(): void | Promise { + override register(): void | Promise { + this.navigationTabsService.welcomeContainer.add(WelcomeNewConnection, undefined, () => this.isConnectionFeatureDisabled(true)); this.menuService.addCreator({ menus: [MENU_CONNECTIONS], - getItems: (context, items) => [...items, ACTION_CONNECTION_CUSTOM], + getItems: (context, items) => [ACTION_CONNECTION_CUSTOM, ...items], }); - this.actionService.addHandler({ - id: 'connection-custom', - isActionApplicable: (context, action) => [ACTION_CONNECTION_CUSTOM].includes(action), - isHidden: (context, action) => { - if (this.connectionsManagerService.createConnectionProjects.length === 0) { - return true; - } + this.menuService.addCreator({ + menus: [MENU_TREE_CREATE_CONNECTION], + getItems: (context, items) => [ACTION_TREE_CREATE_CONNECTION, ...items], + isApplicable: () => !this.isConnectionFeatureDisabled(true), + }); - if (action === ACTION_CONNECTION_CUSTOM) { - return this.customConnectionSettingsService.settings.getValue('disabled'); + this.menuService.addCreator({ + root: true, + contexts: [DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_ELEMENTS_TREE], + isApplicable: context => { + const node = context.get(DATA_CONTEXT_NAV_NODE); + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; + const targetNode = this.treeSelectionService.getFirstSelectedNode( + tree, + getProjectNodeId, + project => project.canEditDataSources, + isProjectNode, + isConnectionFolder, + ); + + if (![isConnectionFolder, isProjectNode].some(check => check(node)) || this.isConnectionFeatureDisabled(true) || targetNode === undefined) { + return false; } - return false; - }, - getLoader: (context, action) => { - return getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey); + return true; }, - handler: async (context, action) => { - switch (action) { - case ACTION_CONNECTION_CUSTOM: { - await this.openConnectionsDialog(); - break; - } + getItems: (context, items) => [ACTION_TREE_CREATE_CONNECTION, ...items], + }); + + this.actionService.addHandler({ + id: 'nav-tree-create-create-connection-handler', + actions: [ACTION_TREE_CREATE_CONNECTION], + getActionInfo: (context, action) => { + const menu = context.get(DATA_CONTEXT_MENU); + if (menu === MENU_ELEMENTS_TREE_TOOLS) { + return { ...action.info, icon: '/icons/plugin_connection_new_sm.svg' }; } + return action.info; }, + getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), + handler: this.createConnectionHandler.bind(this), + }); + + this.actionService.addHandler({ + id: 'connection-custom', + actions: [ACTION_CONNECTION_CUSTOM], + isHidden: (context, action) => this.isConnectionFeatureDisabled(action === ACTION_CONNECTION_CUSTOM), + getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), + handler: this.createConnectionHandler.bind(this), }); } - load(): void | Promise {} + private async createConnectionHandler(context: IDataContextProvider, action: IAction) { + switch (action) { + case ACTION_TREE_CREATE_CONNECTION: { + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; + const projectId = this.treeSelectionService.getSelectedProject(tree, isProjectNode)?.id; + const selectedNode = this.treeSelectionService.getFirstSelectedNode( + tree, + getProjectNodeId, + project => project.canEditDataSources, + isProjectNode, + isConnectionFolder, + ); + const folderPath = selectedNode?.folderId ? getFolderPath(selectedNode.folderId) : undefined; + await this.openConnectionsDialog(projectId, folderPath); + break; + } + case ACTION_CONNECTION_CUSTOM: + await this.openConnectionsDialog(); + break; + } + } - private async openConnectionsDialog() { - await this.commonDialogService.open(CustomConnectionDialog, null); + private isConnectionFeatureDisabled(hasSettings: boolean) { + if (this.connectionsManagerService.createConnectionProjects.length === 0) { + return true; + } + + if (hasSettings) { + return this.customConnectionSettingsService.disabled; + } + + return false; + } + + async openConnectionsDialog(projectId?: string, folderPath?: string): Promise { + await this.commonDialogService.open(DriverSelectorDialog, { + projectId, + folderPath, + }); } } diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnectionSettingsService.ts b/webapp/packages/plugin-connection-custom/src/CustomConnectionSettingsService.ts index c2f72bbbe2..ac24d2ea54 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnectionSettingsService.ts +++ b/webapp/packages/plugin-connection-custom/src/CustomConnectionSettingsService.ts @@ -1,26 +1,46 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { CONNECTIONS_SETTINGS_GROUP } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; +import { ESettingsValueType, SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; -interface Settings { - disabled: boolean; -} - -const settings: Settings = { - disabled: false, -}; +const settings = schema.object({ + 'plugin.connection-custom.disabled': schemaExtra.stringedBoolean().default(false), +}); -@injectable() +@injectable(() => [SettingsProviderService, SettingsManagerService]) export class CustomConnectionSettingsService { - readonly settings: PluginSettings; + get disabled(): boolean { + return this.settings.getValue('plugin.connection-custom.disabled'); + } + readonly settings: SettingsProvider; + + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { + this.settings = this.settingsProviderService.createSettings(settings); + + this.registerSettings(); + } - constructor(private readonly pluginManagerService: PluginManagerService) { - this.settings = this.pluginManagerService.createSettings('connection-custom', 'plugin', settings); + private registerSettings() { + this.settingsManagerService.registerSettings(() => [ + { + key: 'plugin.connection-custom.disabled', + access: { + scope: ['role'], + }, + group: CONNECTIONS_SETTINGS_GROUP, + name: 'plugin_connection_custom_settings_disabled_name', + type: ESettingsValueType.Checkbox, + }, + ]); } } diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.module.css b/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.module.css new file mode 100644 index 0000000000..0e7e6792e3 --- /dev/null +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.module.css @@ -0,0 +1,29 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.icon { + position: relative; +} + +.staticImage { + box-sizing: border-box; + width: 24px; + max-height: 24px; + min-width: 24px; +} + +.indicator { + position: absolute; + bottom: 2px; + right: 0; + width: 14px; + height: 14px; + display: flex; + border-radius: 50%; + background-color: var(--theme-surface); + border: 1px solid var(--theme-surface); +} diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.tsx b/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.tsx new file mode 100644 index 0000000000..c0ba69ca0e --- /dev/null +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.tsx @@ -0,0 +1,47 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { useCallback } from 'react'; + +import { IconOrImage, ListItem, ListItemDescription, ListItemIcon, ListItemName, s, StaticImage, useS, useTranslate } from '@cloudbeaver/core-blocks'; + +import style from './Driver.module.css'; + +export interface IDriver { + id: string; + icon?: string; + name?: string; + description?: string; + driverInstalled?: boolean; +} + +interface Props { + driver: IDriver; + onSelect: (driverId: string) => void; +} + +export const Driver = observer(function Driver({ driver, onSelect }) { + const translate = useTranslate(); + const select = useCallback(() => onSelect(driver.id), [driver.id, onSelect]); + const styles = useS(style); + + return ( + + + + {!driver.driverInstalled && ( +
+ +
+ )} +
+ {driver.name} + {driver.description} +
+ ); +}); diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelector.module.css b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelector.module.css new file mode 100644 index 0000000000..01ff509c5e --- /dev/null +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelector.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.wrapper { + display: flex; + flex-direction: column; + overflow: auto; +} diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelector.tsx b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelector.tsx similarity index 89% rename from webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelector.tsx rename to webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelector.tsx index c886738aab..3e299b264d 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelector.tsx +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelector.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,8 +10,8 @@ import { useMemo, useState } from 'react'; import { ItemList, ItemListSearch, s, useFocus, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { Driver, IDriver } from './Driver'; -import style from './DriverSelector.m.css'; +import { Driver, type IDriver } from './Driver.js'; +import style from './DriverSelector.module.css'; interface Props { drivers: IDriver[]; diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.module.css b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.module.css new file mode 100644 index 0000000000..8a23c699da --- /dev/null +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.module.css @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.driverSelector { + flex: 1; +} diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx new file mode 100644 index 0000000000..f3c2db1d46 --- /dev/null +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx @@ -0,0 +1,42 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { CommonDialogBody, CommonDialogHeader, CommonDialogWrapper, s, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import type { DialogComponent } from '@cloudbeaver/core-dialogs'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; + +import { DriverSelector } from './DriverSelector.js'; +import styles from './DriverSelectorDialog.module.css'; +import { useDriverSelectorDialog } from './useDriverSelectorDialog.js'; + +type Payload = { + projectId?: string; + folderPath?: string; +}; + +export const DriverSelectorDialog: DialogComponent = observer(function DriverSelectorDialog({ rejectDialog, payload }) { + const translate = useTranslate(); + const style = useS(styles); + useResource(DriverSelectorDialog, ProjectInfoResource, CachedMapAllKey, { forceSuspense: true }); + const dialog = useDriverSelectorDialog({ + projectId: payload.projectId, + folderPath: payload.folderPath, + onSelect: rejectDialog, + }); + + return ( + + + + + + + ); +}); diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts b/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts new file mode 100644 index 0000000000..dbba121c82 --- /dev/null +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts @@ -0,0 +1,71 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { action, computed, observable } from 'mobx'; + +import { useObservableRef, usePermission, useResource } from '@cloudbeaver/core-blocks'; +import { ConnectionsManagerService, DBDriverResource } from '@cloudbeaver/core-connections'; +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { EAdminPermission } from '@cloudbeaver/core-root'; +import { PublicConnectionFormService } from '@cloudbeaver/plugin-connections'; + +import type { IDriver } from './Driver.js'; + +interface State { + readonly drivers: IDriver[]; + select(driverId: string): Promise; +} + +interface DriverSelectorDialogArgs { + onSelect?: () => void; + projectId: string | undefined; + folderPath: string | undefined; +} + +export function useDriverSelectorDialog({ onSelect, projectId, folderPath }: DriverSelectorDialogArgs) { + const isAdmin = usePermission(EAdminPermission.admin); + const notificationService = useService(NotificationService); + const connectionsManagerService = useService(ConnectionsManagerService); + const publicConnectionFormService = useService(PublicConnectionFormService); + const dbDriverResource = useResource(useDriverSelectorDialog, DBDriverResource, CachedMapAllKey); + + const state: State = useObservableRef( + () => ({ + get drivers() { + return this.dbDriverResource.resource.enabledDrivers.filter(driver => { + if (this.isAdmin) { + return true; + } + + return driver.driverInstalled; + }); + }, + async select(driverId: string) { + const projects = this.connectionsManagerService.createConnectionProjects; + const drivers = this.drivers.map(driver => driver.id); + + if (projects.length === 0) { + this.notificationService.logError({ title: 'core_projects_no_default_project' }); + return; + } + + const selectedProjectId = projects.find(project => project.id === projectId)?.id || projects[0]!.id; + const state = await this.publicConnectionFormService.open(selectedProjectId, { driverId, folder: folderPath }, drivers); + + if (state) { + onSelect?.(); + } + }, + }), + { select: action.bound, drivers: computed, dbDriverResource: observable.ref }, + { notificationService, connectionsManagerService, publicConnectionFormService, dbDriverResource, isAdmin }, + ); + + return state; +} diff --git a/webapp/packages/plugin-connection-custom/src/LocaleService.ts b/webapp/packages/plugin-connection-custom/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-connection-custom/src/LocaleService.ts +++ b/webapp/packages/plugin-connection-custom/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-connection-custom/src/WelcomeNewConnection.tsx b/webapp/packages/plugin-connection-custom/src/WelcomeNewConnection.tsx new file mode 100644 index 0000000000..5c7e3b5051 --- /dev/null +++ b/webapp/packages/plugin-connection-custom/src/WelcomeNewConnection.tsx @@ -0,0 +1,30 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Cell, IconOrImage, useTranslate } from '@cloudbeaver/core-blocks'; +import { observer } from 'mobx-react-lite'; +import { ACTION_CONNECTION_CUSTOM } from './Actions/ACTION_CONNECTION_CUSTOM.js'; +import { useService } from '@cloudbeaver/core-di'; +import { CustomConnectionPluginBootstrap } from './CustomConnectionPluginBootstrap.js'; + +export const WelcomeNewConnection = observer(function WelcomeNewConnection() { + const customConnectionPluginBootstrap = useService(CustomConnectionPluginBootstrap); + const translate = useTranslate(); + return ( + + ); +}); diff --git a/webapp/packages/plugin-connection-custom/src/index.ts b/webapp/packages/plugin-connection-custom/src/index.ts index faec463c06..fd51289924 100644 --- a/webapp/packages/plugin-connection-custom/src/index.ts +++ b/webapp/packages/plugin-connection-custom/src/index.ts @@ -1,5 +1,14 @@ -import { customConnectionPluginManifest } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { customConnectionPluginManifest } from './manifest.js'; export default customConnectionPluginManifest; -export * from './CustomConnectionSettingsService'; +export * from './CustomConnectionSettingsService.js'; diff --git a/webapp/packages/plugin-connection-custom/src/locales/en.ts b/webapp/packages/plugin-connection-custom/src/locales/en.ts index 45dbbe3f13..12d4cb1a1c 100644 --- a/webapp/packages/plugin-connection-custom/src/locales/en.ts +++ b/webapp/packages/plugin-connection-custom/src/locales/en.ts @@ -1,21 +1,5 @@ export default [ - ['customConnection_connectionType_custom', 'Parameters'], - ['customConnection_connectionType_url', 'URL'], - ['customConnection_options', 'Options'], - ['customConnection_properties', 'Driver Properties'], - ['customConnection_custom_name', 'Name'], - ['customConnection_custom_host', 'Host'], - ['customConnection_custom_obligatory', '(obligatory)'], - ['customConnection_custom_port', 'Port'], - ['customConnection_custom_server_name', 'Server name'], - ['customConnection_custom_database', 'Database'], - ['customConnection_custom_server', 'Server'], - ['customConnection_url_JDBC', 'JDBC URL'], - ['customConnection_folder', 'Folder'], - ['customConnection_userName', 'User name'], - ['customConnection_Password', 'Password'], - ['customConnection_test', 'Test Connection'], - ['customConnection_create', 'Create'], - ['customConnection_create_error', 'Сreate connection error'], ['plugin_connection_custom_action_custom_label', 'New Connection'], + ['plugin_connection_custom_action_custom_tooltip', 'Create a new connection'], + ['plugin_connection_custom_settings_disabled_name', 'Disable custom connections'], ]; diff --git a/webapp/packages/plugin-connection-custom/src/locales/fr.ts b/webapp/packages/plugin-connection-custom/src/locales/fr.ts new file mode 100644 index 0000000000..67f2e803ba --- /dev/null +++ b/webapp/packages/plugin-connection-custom/src/locales/fr.ts @@ -0,0 +1,5 @@ +export default [ + ['plugin_connection_custom_action_custom_label', 'Nouvelle connexion'], + ['plugin_connection_custom_action_custom_tooltip', 'Créer une nouvelle connexion'], + ['plugin_connection_custom_settings_disabled_name', 'Désactiver les connexions personnalisées'], +]; diff --git a/webapp/packages/plugin-connection-custom/src/locales/it.ts b/webapp/packages/plugin-connection-custom/src/locales/it.ts index 2415200e60..12d4cb1a1c 100644 --- a/webapp/packages/plugin-connection-custom/src/locales/it.ts +++ b/webapp/packages/plugin-connection-custom/src/locales/it.ts @@ -1,21 +1,5 @@ export default [ - ['customConnection_connectionType_custom', 'Parametri'], - ['customConnection_connectionType_url', 'URL'], - ['customConnection_options', 'Opzioni'], - ['customConnection_properties', 'Proprietà del driver'], - ['customConnection_custom_name', 'Nome'], - ['customConnection_custom_host', 'Host'], - ['customConnection_custom_obligatory', '(obbligatorio)'], - ['customConnection_custom_port', 'Porta'], - ['customConnection_custom_server_name', 'Server name'], - ['customConnection_custom_database', 'Database'], - ['customConnection_custom_server', 'Server'], - ['customConnection_url_JDBC', 'JDBC URL'], - ['customConnection_folder', 'Folder'], - ['customConnection_userName', 'User name'], - ['customConnection_Password', 'Password'], - ['customConnection_test', 'Prova la connessione'], - ['customConnection_create', 'Crea'], - ['customConnection_create_error', 'Errore di creazione connessione'], ['plugin_connection_custom_action_custom_label', 'New Connection'], + ['plugin_connection_custom_action_custom_tooltip', 'Create a new connection'], + ['plugin_connection_custom_settings_disabled_name', 'Disable custom connections'], ]; diff --git a/webapp/packages/plugin-connection-custom/src/locales/ru.ts b/webapp/packages/plugin-connection-custom/src/locales/ru.ts index 6479686a49..a1837c3e1c 100644 --- a/webapp/packages/plugin-connection-custom/src/locales/ru.ts +++ b/webapp/packages/plugin-connection-custom/src/locales/ru.ts @@ -1,21 +1,5 @@ export default [ - ['customConnection_connectionType_custom', 'Параметры'], - ['customConnection_connectionType_url', 'URL'], - ['customConnection_options', 'Подключение'], - ['customConnection_properties', 'Параметры драйвера'], - ['customConnection_custom_name', 'Имя'], - ['customConnection_custom_host', 'Хост'], - ['customConnection_custom_obligatory', '(обязательное)'], - ['customConnection_custom_port', 'Порт'], - ['customConnection_custom_server_name', 'Имя сервера'], - ['customConnection_custom_database', 'База'], - ['customConnection_custom_server', 'Сервер'], - ['customConnection_url_JDBC', 'JDBC URL'], - ['customConnection_folder', 'Папка'], - ['customConnection_userName', 'Пользователь'], - ['customConnection_Password', 'Пароль'], - ['customConnection_test', 'Проверить'], - ['customConnection_create', 'Создать'], - ['customConnection_create_error', 'Ошибка при создании подключения'], ['plugin_connection_custom_action_custom_label', 'Новое подключение'], + ['plugin_connection_custom_action_custom_tooltip', 'Создать новое подключение'], + ['plugin_connection_custom_settings_disabled_name', 'Отключить пользовательские подключения'], ]; diff --git a/webapp/packages/plugin-connection-custom/src/locales/vi.ts b/webapp/packages/plugin-connection-custom/src/locales/vi.ts new file mode 100644 index 0000000000..bfa9f78400 --- /dev/null +++ b/webapp/packages/plugin-connection-custom/src/locales/vi.ts @@ -0,0 +1,4 @@ +export default [ + ['plugin_connection_custom_action_custom_label', 'Kết nối mới'], + ['plugin_connection_custom_action_custom_tooltip', 'Tạo một kết nối mới'], +]; diff --git a/webapp/packages/plugin-connection-custom/src/locales/zh.ts b/webapp/packages/plugin-connection-custom/src/locales/zh.ts index ee4fdd2306..8188521e8f 100644 --- a/webapp/packages/plugin-connection-custom/src/locales/zh.ts +++ b/webapp/packages/plugin-connection-custom/src/locales/zh.ts @@ -1,21 +1,5 @@ export default [ - ['customConnection_connectionType_custom', '参数'], - ['customConnection_connectionType_url', 'URL'], - ['customConnection_options', '选项'], - ['customConnection_properties', '驱动属性'], - ['customConnection_custom_name', '名称'], - ['customConnection_custom_host', '主机'], - ['customConnection_custom_obligatory', '(强制性的)'], - ['customConnection_custom_port', '端口'], - ['customConnection_custom_server_name', 'Server name'], - ['customConnection_custom_database', '数据库'], - ['customConnection_custom_server', '服务器'], - ['customConnection_url_JDBC', 'JDBC URL'], - ['customConnection_folder', 'Folder'], - ['customConnection_userName', '用户名称'], - ['customConnection_Password', '密码'], - ['customConnection_test', '测试连接'], - ['customConnection_create', '创建'], - ['customConnection_create_error', '创建连接失败'], - ['plugin_connection_custom_action_custom_label', 'New Connection'], + ['plugin_connection_custom_action_custom_label', '新建连接'], + ['plugin_connection_custom_action_custom_tooltip', '创建一个新的连接'], + ['plugin_connection_custom_settings_disabled_name', '禁用自定义连接'], ]; diff --git a/webapp/packages/plugin-connection-custom/src/manifest.ts b/webapp/packages/plugin-connection-custom/src/manifest.ts index 0364a343d6..b8917ac2e0 100644 --- a/webapp/packages/plugin-connection-custom/src/manifest.ts +++ b/webapp/packages/plugin-connection-custom/src/manifest.ts @@ -1,20 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { CustomConnectionPluginBootstrap } from './CustomConnectionPluginBootstrap'; -import { CustomConnectionSettingsService } from './CustomConnectionSettingsService'; -import { LocaleService } from './LocaleService'; - export const customConnectionPluginManifest: PluginManifest = { info: { name: 'Custom connection plugin', }, - - providers: [LocaleService, CustomConnectionPluginBootstrap, CustomConnectionSettingsService], }; diff --git a/webapp/packages/plugin-connection-custom/src/module.ts b/webapp/packages/plugin-connection-custom/src/module.ts new file mode 100644 index 0000000000..57d2976bc3 --- /dev/null +++ b/webapp/packages/plugin-connection-custom/src/module.ts @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { LocaleService } from './LocaleService.js'; +import { CustomConnectionSettingsService } from './CustomConnectionSettingsService.js'; +import { CustomConnectionPluginBootstrap } from './CustomConnectionPluginBootstrap.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-connection-custom', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Dependency, proxy(CustomConnectionSettingsService)) + .addSingleton(Bootstrap, proxy(CustomConnectionPluginBootstrap)) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(CustomConnectionPluginBootstrap) + .addSingleton(CustomConnectionSettingsService); + }, +}); diff --git a/webapp/packages/plugin-connection-custom/tsconfig.json b/webapp/packages/plugin-connection-custom/tsconfig.json index 730780b131..9a069e11dd 100644 --- a/webapp/packages/plugin-connection-custom/tsconfig.json +++ b/webapp/packages/plugin-connection-custom/tsconfig.json @@ -1,46 +1,65 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-connections/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-connections" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-projects/tsconfig.json" + "path": "../core-navigation-tree" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-projects" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-view/tsconfig.json" + "path": "../core-root" + }, + { + "path": "../core-settings" + }, + { + "path": "../core-utils" + }, + { + "path": "../core-view" + }, + { + "path": "../plugin-connections" + }, + { + "path": "../plugin-navigation-tabs" + }, + { + "path": "../plugin-navigation-tree" } ], "include": [ @@ -52,7 +71,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-connection-search/package.json b/webapp/packages/plugin-connection-search/package.json index d811f1d3ce..4ac41f4e3d 100644 --- a/webapp/packages/plugin-connection-search/package.json +++ b/webapp/packages/plugin-connection-search/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-connection-search", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,37 +11,43 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-connections": "~0.1.0", - "@cloudbeaver/core-authentication": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-projects": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-projects": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-settings": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-connections": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-connection-search/src/Actions/ACTION_CONNECTION_SEARCH.ts b/webapp/packages/plugin-connection-search/src/Actions/ACTION_CONNECTION_SEARCH.ts index 78187f10ee..90ac6e086c 100644 --- a/webapp/packages/plugin-connection-search/src/Actions/ACTION_CONNECTION_SEARCH.ts +++ b/webapp/packages/plugin-connection-search/src/Actions/ACTION_CONNECTION_SEARCH.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connection-search/src/ConnectionSearchSettingsService.ts b/webapp/packages/plugin-connection-search/src/ConnectionSearchSettingsService.ts index 7ce7e5b1e3..f561118ba1 100644 --- a/webapp/packages/plugin-connection-search/src/ConnectionSearchSettingsService.ts +++ b/webapp/packages/plugin-connection-search/src/ConnectionSearchSettingsService.ts @@ -1,26 +1,47 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { CONNECTIONS_SETTINGS_GROUP } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; +import { ESettingsValueType, SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; -interface Settings { - disabled: boolean; -} - -const settings: Settings = { - disabled: false, -}; +const settings = schema.object({ + 'plugin.connection-search.disabled': schemaExtra.stringedBoolean().default(false), +}); -@injectable() +@injectable(() => [SettingsProviderService, SettingsManagerService]) export class ConnectionSearchSettingsService { - readonly settings: PluginSettings; + get disabled(): boolean { + return this.settings.getValue('plugin.connection-search.disabled'); + } + readonly settings: SettingsProvider; + + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { + this.settings = this.settingsProviderService.createSettings(settings); + + this.registerSettings(); + } - constructor(private readonly pluginManagerService: PluginManagerService) { - this.settings = this.pluginManagerService.createSettings('connection-search', 'plugin', settings); + private registerSettings() { + this.settingsManagerService.registerSettings(() => [ + { + group: CONNECTIONS_SETTINGS_GROUP, + key: 'plugin.connection-search.disabled', + access: { + scope: ['server', 'role'], + }, + type: ESettingsValueType.Checkbox, + name: 'plugin_connection_search_settings_disable', + description: 'plugin_connection_search_settings_disable_description', + }, + ]); } } diff --git a/webapp/packages/plugin-connection-search/src/LocaleService.ts b/webapp/packages/plugin-connection-search/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-connection-search/src/LocaleService.ts +++ b/webapp/packages/plugin-connection-search/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-connection-search/src/Search/ConnectionSearchService.ts b/webapp/packages/plugin-connection-search/src/Search/ConnectionSearchService.ts index 8a72233de6..7adb038c83 100644 --- a/webapp/packages/plugin-connection-search/src/Search/ConnectionSearchService.ts +++ b/webapp/packages/plugin-connection-search/src/Search/ConnectionSearchService.ts @@ -1,44 +1,51 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 } from 'mobx'; -import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; +import { action, makeObservable, observable } from 'mobx'; + +import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource, ConnectionsManagerService, createConnectionParam } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { injectable, IServiceProvider } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; -import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; +import { ExecutorInterrupter, type IExecutorHandler } from '@cloudbeaver/core-executor'; import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk'; -import { OptionsPanelService } from '@cloudbeaver/core-ui'; -import { ConnectionFormService, ConnectionFormState, IConnectionFormState } from '@cloudbeaver/plugin-connections'; +import { OptionsPanelService, type OptionsPanelCloseEventData } from '@cloudbeaver/core-ui'; +import { ConnectionFormService, ConnectionFormState, getConnectionFormOptionsPart } from '@cloudbeaver/plugin-connections'; -import { SearchDatabase } from './SearchDatabase'; +const SearchDatabase = importLazyComponent(() => import('./SearchDatabase.js').then(module => module.SearchDatabase)); const formGetter = () => SearchDatabase; -@injectable() +@injectable(() => [ + NotificationService, + ConnectionInfoResource, + IServiceProvider, + OptionsPanelService, + ConnectionFormService, + CommonDialogService, + ConnectionsManagerService, +]) export class ConnectionSearchService { hosts = 'localhost'; databases: AdminConnectionSearchInfo[]; disabled = false; - formState: IConnectionFormState | null = null; + formState: ConnectionFormState | null = null; constructor( private readonly notificationService: NotificationService, private readonly connectionInfoResource: ConnectionInfoResource, - private readonly connectionFormService: ConnectionFormService, + private readonly serviceProvider: IServiceProvider, private readonly optionsPanelService: OptionsPanelService, + private readonly connectionFormService: ConnectionFormService, private readonly commonDialogService: CommonDialogService, - private readonly projectsService: ProjectsService, - private readonly projectInfoResource: ProjectInfoResource, private readonly connectionsManagerService: ConnectionsManagerService, ) { this.optionsPanelService.closeTask.addHandler(this.closeHandler); @@ -53,6 +60,7 @@ export class ConnectionSearchService { databases: observable, disabled: observable, formState: observable.shallow, + select: action, }); } @@ -92,38 +100,42 @@ export class ConnectionSearchService { } } - private readonly closeHandler: IExecutorHandler = async (data, contexts) => { - const isDialogClosed = await this.showUnsavedChangesDialog(); + private readonly closeHandler: IExecutorHandler = async (data, contexts) => { + if (data === 'before') { + const isDialogClosed = await this.showUnsavedChangesDialog(); - if (!isDialogClosed) { - ExecutorInterrupter.interrupt(contexts); - return; - } + if (!isDialogClosed) { + ExecutorInterrupter.interrupt(contexts); + return; + } - this.clearFormState(); - this.close(); + this.clearFormState(); + this.close(); + } }; + private get optionsPart() { + return this.formState ? getConnectionFormOptionsPart(this.formState) : null; + } + private async showUnsavedChangesDialog(): Promise { if ( !this.formState || !this.optionsPanelService.isOpen(formGetter) || - (this.formState.config.connectionId && - this.formState.projectId !== null && - !this.connectionInfoResource.has(createConnectionParam(this.formState.projectId, this.formState.config.connectionId))) + (this.optionsPart?.state.connectionId && + this.formState.state.projectId !== null && + !this.connectionInfoResource.has(createConnectionParam(this.formState.state.projectId, this.optionsPart.state.connectionId))) ) { return true; } - const state = await this.formState.checkFormState(); - - if (!state?.edited) { + if (!this.formState.isChanged) { return true; } const result = await this.commonDialogService.open(ConfirmationDialog, { - title: 'connections_public_connection_edit_cancel_title', - message: 'connections_public_connection_edit_cancel_message', + title: 'plugin_connections_connection_edit_cancel_title', + message: 'plugin_connections_connection_edit_cancel_message', confirmActionText: 'ui_processing_ok', }); @@ -134,15 +146,15 @@ export class ConnectionSearchService { this.hosts = hosts; } - saveConnection() { + saveConnection(): void { this.goBack(); } - goBack() { + goBack(): void { this.clearFormState(); } - select(database: AdminConnectionSearchInfo): void { + async select(database: AdminConnectionSearchInfo): Promise { const projects = this.connectionsManagerService.createConnectionProjects; if (projects.length === 0) { @@ -150,28 +162,23 @@ export class ConnectionSearchService { return; } - if (!this.formState) { - this.formState = new ConnectionFormState( - this.projectsService, - this.projectInfoResource, - this.connectionFormService, - this.connectionInfoResource, - ); + this.formState?.dispose(); + this.formState = new ConnectionFormState(this.serviceProvider, this.connectionFormService, { + projectId: projects[0]!.id, + availableDrivers: database.possibleDrivers, + type: 'public', + requiredNetworkHandlersIds: [], + connectionId: undefined, + }); - this.formState.closeTask.addHandler(this.goBack.bind(this)); - } + await this.optionsPart?.load(); + await this.optionsPart?.setDriverId(database.defaultDriver); + + this.optionsPart!.state.host = database.host; + this.optionsPart!.state.port = String(database.port); + this.optionsPart!.state.driverId = database.defaultDriver; - this.formState - .setOptions('create', 'public') - .setConfig(projects[0].id, { - ...this.connectionInfoResource.getEmptyConfig(), - driverId: database.defaultDriver, - host: database.host, - port: `${database.port}`, - }) - .setAvailableDrivers(database.possibleDrivers); - - this.formState.load(); + this.formState.disposeTask.addHandler(this.goBack.bind(this)); } private clearFormState() { diff --git a/webapp/packages/plugin-connection-search/src/Search/Database.module.css b/webapp/packages/plugin-connection-search/src/Search/Database.module.css new file mode 100644 index 0000000000..59c276f5ed --- /dev/null +++ b/webapp/packages/plugin-connection-search/src/Search/Database.module.css @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.listItemIcon { + position: relative; + min-width: 80px; + justify-content: flex-end; +} + +.staticImage { + composes: theme-background-surface theme-border-color-surface from global; + box-sizing: border-box; + width: 32px; + border-radius: 50%; + border: solid 2px; + + &:hover { + z-index: 1; + } + &:not(:first-child) { + margin-left: -20px; + } +} diff --git a/webapp/packages/plugin-connection-search/src/Search/Database.tsx b/webapp/packages/plugin-connection-search/src/Search/Database.tsx index e045f2b47d..77693e6551 100644 --- a/webapp/packages/plugin-connection-search/src/Search/Database.tsx +++ b/webapp/packages/plugin-connection-search/src/Search/Database.tsx @@ -1,41 +1,19 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; import { useCallback, useMemo } from 'react'; -import styled, { css } from 'reshadow'; -import { ListItem, ListItemIcon, ListItemName, StaticImage } from '@cloudbeaver/core-blocks'; +import { ListItem, ListItemIcon, ListItemName, s, StaticImage, useS } from '@cloudbeaver/core-blocks'; import { DBDriverResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk'; -const styles = css` - ListItemIcon { - position: relative; - min-width: 80px; - justify-content: flex-end; - } - - StaticImage { - composes: theme-background-surface theme-border-color-surface from global; - box-sizing: border-box; - width: 32px; - border-radius: 50%; - border: solid 2px; - - &:hover { - z-index: 1; - } - &:not(:first-child) { - margin-left: -20px; - } - } -`; +import style from './Database.module.css'; interface Props { database: AdminConnectionSearchInfo; @@ -43,6 +21,7 @@ interface Props { } export const Database = observer(function Database({ database, onSelect }) { + const styles = useS(style); const drivers = useService(DBDriverResource); const select = useCallback(() => onSelect(database), [database]); const orderedDrivers = useMemo( @@ -62,14 +41,14 @@ export const Database = observer(function Database({ database, onSelect } const host = database.host + ':' + database.port; const name = database.displayName !== database.host ? database.displayName + ' (' + host + ')' : host; - return styled(styles)( + return ( - + {orderedDrivers.map(driverId => ( - + ))} {name} - , + ); }); diff --git a/webapp/packages/plugin-connection-search/src/Search/DatabaseList.module.css b/webapp/packages/plugin-connection-search/src/Search/DatabaseList.module.css new file mode 100644 index 0000000000..229d313f6f --- /dev/null +++ b/webapp/packages/plugin-connection-search/src/Search/DatabaseList.module.css @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.form { + composes: theme-background-surface theme-text-on-surface from global; + height: 100%; + display: flex; + flex-direction: column; + overflow: auto; +} diff --git a/webapp/packages/plugin-connection-search/src/Search/DatabaseList.tsx b/webapp/packages/plugin-connection-search/src/Search/DatabaseList.tsx index 60a6996d26..bdb313b5f4 100644 --- a/webapp/packages/plugin-connection-search/src/Search/DatabaseList.tsx +++ b/webapp/packages/plugin-connection-search/src/Search/DatabaseList.tsx @@ -1,28 +1,18 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useCallback, useState } from 'react'; -import styled, { css } from 'reshadow'; -import { Form, ItemList, ItemListSearch, TextPlaceholder, useFocus, useTranslate } from '@cloudbeaver/core-blocks'; +import { Form, ItemList, ItemListSearch, s, TextPlaceholder, useFocus, useS, useTranslate } from '@cloudbeaver/core-blocks'; import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk'; -import { Database } from './Database'; - -const styles = css` - Form { - composes: theme-background-surface theme-text-on-surface from global; - flex: 1; - display: flex; - flex-direction: column; - overflow: auto; - } -`; +import { Database } from './Database.js'; +import style from './DatabaseList.module.css'; interface Props { databases: AdminConnectionSearchInfo[]; @@ -35,6 +25,7 @@ interface Props { } export const DatabaseList = observer(function DatabaseList({ databases, hosts, disabled, className, onSelect, onChange, onSearch }) { + const styles = useS(style); const [focusedRef] = useFocus({ focusFirstChild: true }); const translate = useTranslate(); const [isSearched, setIsSearched] = useState(false); @@ -47,14 +38,15 @@ export const DatabaseList = observer(function DatabaseList({ databases, h } }, [onSearch]); - const placeholderMessage = isSearched ? 'connections_not_found' : 'connections_administration_search_database_tip'; + const placeholderMessage = isSearched ? 'connections_not_found' : 'core_connections_search_database_tip'; - return styled(styles)( -
+ return ( + @@ -64,6 +56,6 @@ export const DatabaseList = observer(function DatabaseList({ databases, h ))} {!databases.length && {translate(placeholderMessage)}} - , + ); }); diff --git a/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.module.css b/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.module.css new file mode 100644 index 0000000000..1e6148d5d7 --- /dev/null +++ b/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.module.css @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.loader { + height: 100%; +} diff --git a/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.tsx b/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.tsx index 2fac5c612d..269f2f6d3a 100644 --- a/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.tsx +++ b/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.tsx @@ -1,14 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled, { css } from 'reshadow'; -import { Loader, useResource } from '@cloudbeaver/core-blocks'; +import { Loader, s, useResource, useS } from '@cloudbeaver/core-blocks'; import { DBDriverResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; @@ -16,16 +15,12 @@ import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk'; import { ConnectionFormLoader } from '@cloudbeaver/plugin-connections'; -import { ConnectionSearchService } from './ConnectionSearchService'; -import { DatabaseList } from './DatabaseList'; - -const styles = css` - Loader { - height: 100%; - } -`; +import { ConnectionSearchService } from './ConnectionSearchService.js'; +import { DatabaseList } from './DatabaseList.js'; +import style from './SearchDatabase.module.css'; export const SearchDatabase: React.FC = observer(function SearchDatabase() { + const styles = useS(style); const connectionSearchService = useService(ConnectionSearchService); useResource(SearchDatabase, ProjectInfoResource, CachedMapAllKey); @@ -36,14 +31,14 @@ export const SearchDatabase: React.FC = observer(function SearchDatabase() { } if (connectionSearchService.formState) { - return styled(styles)( - + return ( + connectionSearchService.saveConnection()} onCancel={() => connectionSearchService.goBack()} /> - , + ); } diff --git a/webapp/packages/plugin-connection-search/src/SearchConnectionPluginBootstrap.ts b/webapp/packages/plugin-connection-search/src/SearchConnectionPluginBootstrap.ts index 8bb60f8fdf..08f2854a27 100644 --- a/webapp/packages/plugin-connection-search/src/SearchConnectionPluginBootstrap.ts +++ b/webapp/packages/plugin-connection-search/src/SearchConnectionPluginBootstrap.ts @@ -1,24 +1,31 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { EAdminPermission } from '@cloudbeaver/core-authentication'; import { ConnectionsManagerService } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; -import { PermissionsService } from '@cloudbeaver/core-root'; -import { ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view'; -import { MENU_CONNECTIONS } from '@cloudbeaver/plugin-connections'; +import { EAdminPermission, PermissionsService } from '@cloudbeaver/core-root'; +import { ActionService, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; +import { MENU_CONNECTIONS, MENU_TREE_CREATE_CONNECTION } from '@cloudbeaver/plugin-connections'; -import { ACTION_CONNECTION_SEARCH } from './Actions/ACTION_CONNECTION_SEARCH'; -import { ConnectionSearchSettingsService } from './ConnectionSearchSettingsService'; -import { ConnectionSearchService } from './Search/ConnectionSearchService'; +import { ACTION_CONNECTION_SEARCH } from './Actions/ACTION_CONNECTION_SEARCH.js'; +import { ConnectionSearchSettingsService } from './ConnectionSearchSettingsService.js'; +import { ConnectionSearchService } from './Search/ConnectionSearchService.js'; -@injectable() +@injectable(() => [ + PermissionsService, + ProjectInfoResource, + ConnectionSearchService, + ConnectionsManagerService, + MenuService, + ActionService, + ConnectionSearchSettingsService, +]) export class SearchConnectionPluginBootstrap extends Bootstrap { constructor( private readonly permissionsService: PermissionsService, @@ -32,30 +39,32 @@ export class SearchConnectionPluginBootstrap extends Bootstrap { super(); } - register(): void | Promise { + override register(): void | Promise { this.menuService.addCreator({ - isApplicable: context => context.tryGet(DATA_CONTEXT_MENU) === MENU_CONNECTIONS, + menus: [MENU_CONNECTIONS, MENU_TREE_CREATE_CONNECTION], getItems: (context, items) => [...items, ACTION_CONNECTION_SEARCH], + orderItems: (context, items) => { + items.push(...menuExtractItems(items, [ACTION_CONNECTION_SEARCH])); + return items; + }, }); this.actionService.addHandler({ id: 'connection-search', - isActionApplicable: (context, action) => [ACTION_CONNECTION_SEARCH].includes(action), + actions: [ACTION_CONNECTION_SEARCH], isHidden: (context, action) => { if (this.connectionsManagerService.createConnectionProjects.length === 0 || !this.permissionsService.has(EAdminPermission.admin)) { return true; } if (action === ACTION_CONNECTION_SEARCH) { - return this.connectionSearchSettingsService.settings.getValue('disabled'); + return this.connectionSearchSettingsService.disabled; } return false; }, - getLoader: (context, action) => { - return getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey); - }, - handler: async (context, action) => { + getLoader: () => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), + handler: (context, action) => { switch (action) { case ACTION_CONNECTION_SEARCH: { this.connectionSearchService.open(); @@ -65,6 +74,4 @@ export class SearchConnectionPluginBootstrap extends Bootstrap { }, }); } - - load(): void | Promise {} } diff --git a/webapp/packages/plugin-connection-search/src/index.ts b/webapp/packages/plugin-connection-search/src/index.ts index 22f185f033..ce581e33a2 100644 --- a/webapp/packages/plugin-connection-search/src/index.ts +++ b/webapp/packages/plugin-connection-search/src/index.ts @@ -1,5 +1,14 @@ -import { connectionSearchPlugin } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { connectionSearchPlugin } from './manifest.js'; export default connectionSearchPlugin; -export * from './ConnectionSearchSettingsService'; +export * from './ConnectionSearchSettingsService.js'; diff --git a/webapp/packages/plugin-connection-search/src/locales/en.ts b/webapp/packages/plugin-connection-search/src/locales/en.ts index 50ae687c7f..ac18ab13c7 100644 --- a/webapp/packages/plugin-connection-search/src/locales/en.ts +++ b/webapp/packages/plugin-connection-search/src/locales/en.ts @@ -1 +1,5 @@ -export default [['plugin_connection_search_action_search_label', 'Find Local Database']]; +export default [ + ['plugin_connection_search_action_search_label', 'Find Database'], + ['plugin_connection_search_settings_disable', 'Disable "{alias:plugin_connection_search_action_search_label}"'], + ['plugin_connection_search_settings_disable_description', 'Disable the ability to search connections on a specific address'], +]; diff --git a/webapp/packages/plugin-connection-search/src/locales/fr.ts b/webapp/packages/plugin-connection-search/src/locales/fr.ts new file mode 100644 index 0000000000..ac18ab13c7 --- /dev/null +++ b/webapp/packages/plugin-connection-search/src/locales/fr.ts @@ -0,0 +1,5 @@ +export default [ + ['plugin_connection_search_action_search_label', 'Find Database'], + ['plugin_connection_search_settings_disable', 'Disable "{alias:plugin_connection_search_action_search_label}"'], + ['plugin_connection_search_settings_disable_description', 'Disable the ability to search connections on a specific address'], +]; diff --git a/webapp/packages/plugin-connection-search/src/locales/it.ts b/webapp/packages/plugin-connection-search/src/locales/it.ts index 50ae687c7f..ac18ab13c7 100644 --- a/webapp/packages/plugin-connection-search/src/locales/it.ts +++ b/webapp/packages/plugin-connection-search/src/locales/it.ts @@ -1 +1,5 @@ -export default [['plugin_connection_search_action_search_label', 'Find Local Database']]; +export default [ + ['plugin_connection_search_action_search_label', 'Find Database'], + ['plugin_connection_search_settings_disable', 'Disable "{alias:plugin_connection_search_action_search_label}"'], + ['plugin_connection_search_settings_disable_description', 'Disable the ability to search connections on a specific address'], +]; diff --git a/webapp/packages/plugin-connection-search/src/locales/ru.ts b/webapp/packages/plugin-connection-search/src/locales/ru.ts index dfde2fdaf0..6db72ec177 100644 --- a/webapp/packages/plugin-connection-search/src/locales/ru.ts +++ b/webapp/packages/plugin-connection-search/src/locales/ru.ts @@ -1 +1,5 @@ -export default [['plugin_connection_search_action_search_label', 'Найти локальную базу данных']]; +export default [ + ['plugin_connection_search_action_search_label', 'Найти базу данных'], + ['plugin_connection_search_settings_disable', 'Отключить "{alias:plugin_connection_search_action_search_label}"'], + ['plugin_connection_search_settings_disable_description', 'Отключить возможность поиска подключений по заданному адресу'], +]; diff --git a/webapp/packages/plugin-connection-search/src/locales/vi.ts b/webapp/packages/plugin-connection-search/src/locales/vi.ts new file mode 100644 index 0000000000..df453aaa8b --- /dev/null +++ b/webapp/packages/plugin-connection-search/src/locales/vi.ts @@ -0,0 +1,5 @@ +export default [ + ['plugin_connection_search_action_search_label', 'Tìm cơ sở dữ liệu'], + ['plugin_connection_search_settings_disable', 'Tắt "{alias:plugin_connection_search_action_search_label}"'], + ['plugin_connection_search_settings_disable_description', 'Tắt khả năng tìm kiếm kết nối tại một địa chỉ cụ thể'], +]; diff --git a/webapp/packages/plugin-connection-search/src/locales/zh.ts b/webapp/packages/plugin-connection-search/src/locales/zh.ts index 50ae687c7f..ac18ab13c7 100644 --- a/webapp/packages/plugin-connection-search/src/locales/zh.ts +++ b/webapp/packages/plugin-connection-search/src/locales/zh.ts @@ -1 +1,5 @@ -export default [['plugin_connection_search_action_search_label', 'Find Local Database']]; +export default [ + ['plugin_connection_search_action_search_label', 'Find Database'], + ['plugin_connection_search_settings_disable', 'Disable "{alias:plugin_connection_search_action_search_label}"'], + ['plugin_connection_search_settings_disable_description', 'Disable the ability to search connections on a specific address'], +]; diff --git a/webapp/packages/plugin-connection-search/src/manifest.ts b/webapp/packages/plugin-connection-search/src/manifest.ts index 481cb74b90..767724ac55 100644 --- a/webapp/packages/plugin-connection-search/src/manifest.ts +++ b/webapp/packages/plugin-connection-search/src/manifest.ts @@ -1,13 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { ConnectionSearchSettingsService } from './ConnectionSearchSettingsService'; -import { LocaleService } from './LocaleService'; -import { ConnectionSearchService } from './Search/ConnectionSearchService'; -import { SearchConnectionPluginBootstrap } from './SearchConnectionPluginBootstrap'; - export const connectionSearchPlugin: PluginManifest = { info: { name: 'Search connection plugin', }, - providers: [SearchConnectionPluginBootstrap, ConnectionSearchService, LocaleService, ConnectionSearchSettingsService], }; diff --git a/webapp/packages/plugin-connection-search/src/module.ts b/webapp/packages/plugin-connection-search/src/module.ts new file mode 100644 index 0000000000..6b3d3dfcfd --- /dev/null +++ b/webapp/packages/plugin-connection-search/src/module.ts @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { ConnectionSearchService } from './Search/ConnectionSearchService.js'; +import { SearchConnectionPluginBootstrap } from './SearchConnectionPluginBootstrap.js'; +import { LocaleService } from './LocaleService.js'; +import { ConnectionSearchSettingsService } from './ConnectionSearchSettingsService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-connection-search', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Dependency, proxy(ConnectionSearchSettingsService)) + .addSingleton(ConnectionSearchSettingsService) + .addSingleton(ConnectionSearchService) + .addSingleton(Bootstrap, SearchConnectionPluginBootstrap) + .addSingleton(Bootstrap, LocaleService); + }, +}); diff --git a/webapp/packages/plugin-connection-search/tsconfig.json b/webapp/packages/plugin-connection-search/tsconfig.json index 8157be5962..954c2af623 100644 --- a/webapp/packages/plugin-connection-search/tsconfig.json +++ b/webapp/packages/plugin-connection-search/tsconfig.json @@ -1,58 +1,62 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-connections/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-authentication/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-connections" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-executor" }, { - "path": "../core-executor/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-projects" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-projects/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-sdk" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-settings" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-utils" }, { - "path": "../core-view/tsconfig.json" + "path": "../core-view" + }, + { + "path": "../plugin-connections" } ], "include": [ @@ -64,7 +68,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-connection-template/package.json b/webapp/packages/plugin-connection-template/package.json deleted file mode 100644 index 03191c693a..0000000000 --- a/webapp/packages/plugin-connection-template/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@cloudbeaver/plugin-connection-template", - "sideEffects": [ - "src/**/*.css", - "src/**/*.scss", - "public/**/*" - ], - "version": "0.1.0", - "description": "", - "license": "Apache-2.0", - "main": "dist/index.js", - "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", - "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" - }, - "dependencies": { - "@cloudbeaver/plugin-connections": "~0.1.0", - "@cloudbeaver/core-authentication": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-projects": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" - }, - "devDependencies": {} -} diff --git a/webapp/packages/plugin-connection-template/src/Actions/ACTION_CONNECTION_TEMPLATE.ts b/webapp/packages/plugin-connection-template/src/Actions/ACTION_CONNECTION_TEMPLATE.ts deleted file mode 100644 index bd2c259e79..0000000000 --- a/webapp/packages/plugin-connection-template/src/Actions/ACTION_CONNECTION_TEMPLATE.ts +++ /dev/null @@ -1,12 +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 { createAction } from '@cloudbeaver/core-view'; - -export const ACTION_CONNECTION_TEMPLATE = createAction('connection-template', { - label: 'plugin_connection_template_action_connection_template_label', -}); diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionController.ts b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionController.ts deleted file mode 100644 index 65e3512aae..0000000000 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionController.ts +++ /dev/null @@ -1,267 +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 { computed, makeObservable, observable } from 'mobx'; - -import { ErrorDetailsDialog } from '@cloudbeaver/core-blocks'; -import { - Connection, - ConnectionInfoProjectKey, - ConnectionInfoResource, - ConnectionInitConfig, - createConnectionParam, - DatabaseAuthModelsResource, - DBDriver, - DBDriverResource, - USER_NAME_PROPERTY_ID, -} from '@cloudbeaver/core-connections'; -import { IDestructibleController, IInitializableController, injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { ProjectsService } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { DatabaseAuthModel, DetailsError, NetworkHandlerAuthType } from '@cloudbeaver/core-sdk'; -import { errorOf, getUniqueName } from '@cloudbeaver/core-utils'; -import type { IConnectionAuthenticationConfig } from '@cloudbeaver/plugin-connections'; - -import { TemplateConnectionsResource } from '../TemplateConnectionsResource'; -import { TemplateConnectionsService } from '../TemplateConnectionsService'; - -export enum ConnectionStep { - ConnectionTemplateSelect, - Connection, -} - -export interface IConnectionController { - template: Connection | null; - config: IConnectionAuthenticationConfig; - isConnecting: boolean; - onConnect: () => void; -} - -@injectable() -export class ConnectionController implements IInitializableController, IDestructibleController, IConnectionController { - step = ConnectionStep.ConnectionTemplateSelect; - isLoading = true; - isConnecting = false; - template: Connection | null = null; - authModel?: DatabaseAuthModel; - config: IConnectionAuthenticationConfig = { - credentials: {}, - networkHandlersConfig: [], - saveCredentials: false, - }; - - hasDetails = false; - responseMessage: string | null = null; - - private exception: DetailsError | null = null; - private onClose!: () => void; - private isDistructed = false; - - get templateConnections(): Connection[] { - return this.templateConnectionsService.projectTemplates; - } - - get dbDrivers(): Map { - return this.dbDriverResource.data; - } - - get dbDriver(): DBDriver | undefined { - if (!this.template) { - return undefined; - } - return this.dbDrivers.get(this.template.driverId); - } - - get networkHandlers(): string[] { - if (!this.template?.networkHandlersConfig) { - return []; - } - - return this.template.networkHandlersConfig.filter(handler => handler.enabled && !handler.savePassword).map(handler => handler.id); - } - - constructor( - private readonly dbDriverResource: DBDriverResource, - private readonly connectionInfoResource: ConnectionInfoResource, - private readonly templateConnectionsResource: TemplateConnectionsResource, - private readonly templateConnectionsService: TemplateConnectionsService, - private readonly notificationService: NotificationService, - private readonly commonDialogService: CommonDialogService, - private readonly dbAuthModelsResource: DatabaseAuthModelsResource, - private readonly projectsService: ProjectsService, - ) { - this.authModel = undefined; - - makeObservable(this, { - step: observable, - isLoading: observable, - isConnecting: observable, - template: observable, - authModel: observable, - config: observable, - hasDetails: observable, - responseMessage: observable, - networkHandlers: computed, - }); - } - - init(onClose: () => void): void { - this.onClose = onClose; - this.loadTemplateConnections(); - } - - destruct(): void { - this.isDistructed = true; - } - - onStep = (step: ConnectionStep): void => { - this.step = step; - this.clearError(); - - if (step === ConnectionStep.ConnectionTemplateSelect) { - this.template = null; - } - }; - - onConnect = async (): Promise => { - if (!this.template || !this.projectsService.userProject) { - return; - } - - this.isConnecting = true; - this.clearError(); - try { - const connections = await this.connectionInfoResource.load(ConnectionInfoProjectKey(this.projectsService.userProject.id)); - const connectionNames = connections.map(connection => connection.name); - - const uniqueConnectionName = getUniqueName(this.template.name || 'Template connection', connectionNames); - const connection = await this.connectionInfoResource.createFromTemplate(this.template.projectId, this.template.id, uniqueConnectionName); - - try { - await this.connectionInfoResource.init(this.getConfig(connection.projectId, connection.id)); - - this.notificationService.logSuccess({ title: 'Connection is established', message: connection.name }); - this.onClose(); - } catch (exception: any) { - this.showError(exception, 'Failed to establish connection'); - await this.connectionInfoResource.deleteConnection(createConnectionParam(connection)); - } - } catch (exception: any) { - this.showError(exception, 'Failed to establish connection'); - } finally { - this.isConnecting = false; - } - }; - - onTemplateSelect = async (templateId: string): Promise => { - this.template = this.templateConnections.find(template => template.id === templateId)!; - - await this.loadAuthModel(); - this.clearError(); - this.config = { - credentials: {}, - networkHandlersConfig: [], - saveCredentials: false, - }; - - if (this.template.authNeeded) { - const property = this.template.authProperties?.find(property => property.id === USER_NAME_PROPERTY_ID); - - if (property?.value) { - this.config.credentials[USER_NAME_PROPERTY_ID] = property.value; - } - } - - for (const id of this.networkHandlers) { - const handler = this.template.networkHandlersConfig?.find(handler => handler.id === id); - - if (handler && (handler.userName || handler.authType !== NetworkHandlerAuthType.Password)) { - this.config.networkHandlersConfig.push({ - id: handler.id, - authType: handler.authType, - userName: handler.userName, - password: handler.password, - savePassword: handler.savePassword, - }); - } - } - - this.step = ConnectionStep.Connection; - if (!this.authModel) { - this.onConnect(); - } - }; - - onShowDetails = (): void => { - if (this.exception) { - this.commonDialogService.open(ErrorDetailsDialog, this.exception); - } - }; - - private getConfig(projectId: string, connectionId: string) { - const config: ConnectionInitConfig = { - projectId, - connectionId, - }; - - if (Object.keys(this.config.credentials).length > 0) { - config.credentials = this.config.credentials; - config.saveCredentials = this.config.saveCredentials; - } - - if (this.config.networkHandlersConfig.length > 0) { - config.networkCredentials = this.config.networkHandlersConfig; - } - - return config; - } - - private clearError() { - this.responseMessage = null; - this.hasDetails = false; - this.exception = null; - } - - private showError(exception: Error, message: string) { - const detailsError = errorOf(exception, DetailsError); - if (detailsError && !this.isDistructed) { - this.responseMessage = detailsError.message; - this.hasDetails = detailsError.hasDetails(); - this.exception = detailsError; - } else { - this.notificationService.logException(exception, message); - } - } - - private async loadTemplateConnections() { - try { - await this.templateConnectionsResource.load(); - await this.dbDriverResource.load(CachedMapAllKey); - } catch (exception: any) { - this.notificationService.logException(exception, "Can't load database sources"); - } finally { - this.isLoading = false; - } - } - - private async loadAuthModel() { - if (!this.dbDriver || this.dbDriver.anonymousAccess) { - return; - } - - try { - this.isLoading = true; - this.authModel = await this.dbAuthModelsResource.load(this.dbDriver.defaultAuthModel); - } catch (exception: any) { - this.notificationService.logException(exception, "Can't load driver auth model"); - } finally { - this.isLoading = false; - } - } -} diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.m.css b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.m.css deleted file mode 100644 index 1dcf935c11..0000000000 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.m.css +++ /dev/null @@ -1,18 +0,0 @@ -.submittingForm, -.center { - display: flex; - flex: 1; - margin: auto; -} -.center { - box-sizing: border-box; - flex-direction: column; - align-items: center; - justify-content: center; -} -.connectionAuthenticationFormLoader { - align-content: center; -} -.errorMessage { - composes: theme-background-secondary theme-text-on-secondary from global; -} diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx deleted file mode 100644 index e1b19445b5..0000000000 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx +++ /dev/null @@ -1,101 +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 { observer } from 'mobx-react-lite'; - -import { - CommonDialogBody, - CommonDialogFooter, - CommonDialogHeader, - CommonDialogWrapper, - ErrorMessage, - Form, - Loader, - s, - useAdministrationSettings, - useFocus, - useS, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import { useController } from '@cloudbeaver/core-di'; -import type { DialogComponent } from '@cloudbeaver/core-dialogs'; -import { ConnectionAuthenticationFormLoader } from '@cloudbeaver/plugin-connections'; - -import { ConnectionController, ConnectionStep } from './ConnectionController'; -import style from './ConnectionDialog.m.css'; -import { ConnectionDialogFooter } from './ConnectionDialogFooter'; -import { TemplateConnectionSelector } from './TemplateConnectionSelector/TemplateConnectionSelector'; - -export const ConnectionDialog: DialogComponent = observer(function ConnectionDialog({ rejectDialog }) { - const styles = useS(style); - const [focusedRef] = useFocus({ focusFirstChild: true }); - const controller = useController(ConnectionController, rejectDialog); - const translate = useTranslate(); - const { credentialsSavingEnabled } = useAdministrationSettings(); - - let subtitle: string | undefined; - - if (controller.step === ConnectionStep.Connection && controller.template?.name) { - subtitle = controller.template.name; - } - - return ( - - - - {controller.isLoading && } - {!controller.isLoading && controller.step === ConnectionStep.ConnectionTemplateSelect && ( - - )} - {controller.step === ConnectionStep.Connection && - (!controller.authModel ? ( -
- {controller.isConnecting && translate('basicConnection_connectionDialog_connecting_message')} -
- ) : ( -
- - - ))} -
- {controller.step === ConnectionStep.Connection && ( - - {controller.responseMessage && ( - - )} - controller.onStep(ConnectionStep.ConnectionTemplateSelect)} - onConnect={controller.onConnect} - /> - - )} -
- ); -}); diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialogFooter.m.css b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialogFooter.m.css deleted file mode 100644 index 1afe3a27d6..0000000000 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialogFooter.m.css +++ /dev/null @@ -1,12 +0,0 @@ -.controls { - display: flex; - flex: 1; - height: 100%; - align-items: center; - margin: auto; - gap: 24px; -} - -.fill { - flex: 1; -} diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialogFooter.tsx b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialogFooter.tsx deleted file mode 100644 index a117df58d0..0000000000 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialogFooter.tsx +++ /dev/null @@ -1,34 +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 { observer } from 'mobx-react-lite'; - -import { Button, useS, useTranslate } from '@cloudbeaver/core-blocks'; - -import style from './ConnectionDialogFooter.m.css'; - -interface Props { - isConnecting: boolean; - onConnect: () => void; - onBack: () => void; -} - -export const ConnectionDialogFooter = observer(function ConnectionDialogFooter({ isConnecting, onConnect, onBack }) { - const styles = useS(style); - const translate = useTranslate(); - return ( -
-
- - -
- ); -}); diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/TemplateConnectionSelector/TemplateConnectionItem.tsx b/webapp/packages/plugin-connection-template/src/ConnectionDialog/TemplateConnectionSelector/TemplateConnectionItem.tsx deleted file mode 100644 index 9462d1bf5c..0000000000 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/TemplateConnectionSelector/TemplateConnectionItem.tsx +++ /dev/null @@ -1,41 +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 { observer } from 'mobx-react-lite'; -import { useCallback } from 'react'; -import styled, { css } from 'reshadow'; - -import { ListItem, ListItemDescription, ListItemIcon, ListItemName, StaticImage } from '@cloudbeaver/core-blocks'; -import type { Connection, DBDriver } from '@cloudbeaver/core-connections'; - -interface Props { - template: Connection; - dbDriver?: DBDriver; - onSelect: (connectionId: string) => void; -} - -const styles = css` - StaticImage { - box-sizing: border-box; - width: 24px; - max-height: 24px; - } -`; - -export const TemplateConnectionItem = observer(function TemplateConnectionItem({ template, dbDriver, onSelect }) { - const select = useCallback(() => onSelect(template.id), [template]); - - return styled(styles)( - - - - - {template.name} - {template.description} - , - ); -}); diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/TemplateConnectionSelector/TemplateConnectionSelector.tsx b/webapp/packages/plugin-connection-template/src/ConnectionDialog/TemplateConnectionSelector/TemplateConnectionSelector.tsx deleted file mode 100644 index 39fd6c20db..0000000000 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/TemplateConnectionSelector/TemplateConnectionSelector.tsx +++ /dev/null @@ -1,47 +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 { observer } from 'mobx-react-lite'; -import { useMemo, useState } from 'react'; - -import { ItemList, ItemListSearch } from '@cloudbeaver/core-blocks'; -import type { Connection, DBDriver } from '@cloudbeaver/core-connections'; - -import { TemplateConnectionItem } from './TemplateConnectionItem'; - -interface Props { - templateConnections: Connection[]; - dbDrivers: Map; - className?: string; - onSelect: (dbSourceId: string) => void; -} - -export const TemplateConnectionSelector = observer(function TemplateConnectionSelector({ - templateConnections, - dbDrivers, - className, - onSelect, -}) { - const [search, setSearch] = useState(''); - const filteredTemplateConnections = useMemo(() => { - if (!search) { - return templateConnections; - } - return templateConnections.filter(template => template.name.toUpperCase().includes(search.toUpperCase())); - }, [search, templateConnections]); - - return ( - <> - - - {filteredTemplateConnections.map(template => ( - - ))} - - - ); -}); diff --git a/webapp/packages/plugin-connection-template/src/LocaleService.ts b/webapp/packages/plugin-connection-template/src/LocaleService.ts deleted file mode 100644 index e3649a06b4..0000000000 --- a/webapp/packages/plugin-connection-template/src/LocaleService.ts +++ /dev/null @@ -1,35 +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 { 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/plugin-connection-template/src/TemplateConnectionPluginBootstrap.ts b/webapp/packages/plugin-connection-template/src/TemplateConnectionPluginBootstrap.ts deleted file mode 100644 index 1c0039d4e2..0000000000 --- a/webapp/packages/plugin-connection-template/src/TemplateConnectionPluginBootstrap.ts +++ /dev/null @@ -1,72 +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 { AppAuthService } from '@cloudbeaver/core-authentication'; -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; -import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey, getCachedDataResourceLoaderState, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; -import { ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view'; -import { MENU_CONNECTIONS } from '@cloudbeaver/plugin-connections'; - -import { ACTION_CONNECTION_TEMPLATE } from './Actions/ACTION_CONNECTION_TEMPLATE'; -import { ConnectionDialog } from './ConnectionDialog/ConnectionDialog'; -import { TemplateConnectionsResource } from './TemplateConnectionsResource'; -import { TemplateConnectionsService } from './TemplateConnectionsService'; - -@injectable() -export class TemplateConnectionPluginBootstrap extends Bootstrap { - constructor( - private readonly appAuthService: AppAuthService, - private readonly menuService: MenuService, - private readonly actionService: ActionService, - private readonly projectInfoResource: ProjectInfoResource, - private readonly templateConnectionsResource: TemplateConnectionsResource, - private readonly commonDialogService: CommonDialogService, - private readonly templateConnectionsService: TemplateConnectionsService, - private readonly projectsService: ProjectsService, - ) { - super(); - } - - register(): void | Promise { - this.menuService.addCreator({ - isApplicable: context => context.tryGet(DATA_CONTEXT_MENU) === MENU_CONNECTIONS, - getItems: (context, items) => [...items, ACTION_CONNECTION_TEMPLATE], - }); - - this.actionService.addHandler({ - id: 'connection-template', - isActionApplicable: (context, action) => [ACTION_CONNECTION_TEMPLATE].includes(action), - isHidden: () => - !this.appAuthService.authenticated || - !this.projectsService.userProject?.canEditDataSources || - !this.templateConnectionsService.projectTemplates.length, - getLoader: (context, action) => { - return [ - ...this.appAuthService.loaders, - getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), - getCachedDataResourceLoaderState(this.templateConnectionsResource, undefined, undefined), - ]; - }, - handler: async (context, action) => { - switch (action) { - case ACTION_CONNECTION_TEMPLATE: { - await this.openConnectionsDialog(); - break; - } - } - }, - }); - } - - load(): void | Promise {} - - private async openConnectionsDialog() { - await this.commonDialogService.open(ConnectionDialog, null); - } -} diff --git a/webapp/packages/plugin-connection-template/src/TemplateConnectionsResource.ts b/webapp/packages/plugin-connection-template/src/TemplateConnectionsResource.ts deleted file mode 100644 index 0843b0de30..0000000000 --- a/webapp/packages/plugin-connection-template/src/TemplateConnectionsResource.ts +++ /dev/null @@ -1,57 +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 { AppAuthService } from '@cloudbeaver/core-authentication'; -import { Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; -import { CachedDataResource, ResourceKeyUtils } from '@cloudbeaver/core-resource'; -import { SessionDataResource } from '@cloudbeaver/core-root'; -import { GraphQLService } from '@cloudbeaver/core-sdk'; - -@injectable() -export class TemplateConnectionsResource extends CachedDataResource { - constructor( - private readonly graphQLService: GraphQLService, - connectionInfoResource: ConnectionInfoResource, - sessionDataResource: SessionDataResource, - appAuthService: AppAuthService, - ) { - super(() => []); - - this.sync(sessionDataResource); - - appAuthService.requireAuthentication(this); - - connectionInfoResource.onConnectionCreate.addHandler(connection => { - if (connection.template) { - this.markOutdated(); - } - }); - connectionInfoResource.onItemDelete.addHandler(list => { - const isAnyTemplate = connectionInfoResource.get(ResourceKeyUtils.toList(list)).some(connection => connection?.template); - - if (isAnyTemplate) { - this.markOutdated(); - } - }); - } - - protected async loader(): Promise { - const { connections } = await this.graphQLService.sdk.getTemplateConnections({ - includeNetworkHandlersConfig: true, - customIncludeOriginDetails: false, - includeAuthProperties: true, - includeOrigin: false, - includeAuthNeeded: false, - includeCredentialsSaved: false, - includeProperties: false, - includeProviderProperties: false, - customIncludeOptions: false, - }); - return connections; - } -} diff --git a/webapp/packages/plugin-connection-template/src/TemplateConnectionsService.ts b/webapp/packages/plugin-connection-template/src/TemplateConnectionsService.ts deleted file mode 100644 index 3ff2e52b51..0000000000 --- a/webapp/packages/plugin-connection-template/src/TemplateConnectionsService.ts +++ /dev/null @@ -1,28 +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 type { Connection } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; -import { ProjectsService } from '@cloudbeaver/core-projects'; - -import { TemplateConnectionsResource } from './TemplateConnectionsResource'; - -@injectable() -export class TemplateConnectionsService { - get projectTemplates(): Connection[] { - if (this.projectsService.userProject && this.projectsService.activeProjects.includes(this.projectsService.userProject)) { - return this.templateConnectionsResource.data; - } - - // return this.templateConnectionsResource.data - // .filter( - // connection => this.projectsService.activeProjects.some(project => project.id === connection.projectId) - // ); - return []; - } - constructor(private readonly templateConnectionsResource: TemplateConnectionsResource, private readonly projectsService: ProjectsService) {} -} diff --git a/webapp/packages/plugin-connection-template/src/index.ts b/webapp/packages/plugin-connection-template/src/index.ts deleted file mode 100644 index f441811f85..0000000000 --- a/webapp/packages/plugin-connection-template/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { connectionTemplate } from './manifest'; - -export default connectionTemplate; diff --git a/webapp/packages/plugin-connection-template/src/locales/en.ts b/webapp/packages/plugin-connection-template/src/locales/en.ts deleted file mode 100644 index 3d6fd30996..0000000000 --- a/webapp/packages/plugin-connection-template/src/locales/en.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default [ - ['basicConnection_connectionDialog_newConnection', 'New connection'], - ['basicConnection_connectionDialog_title', 'Connect to Database'], - ['basicConnection_connectionDialog_listTitle', 'Database:'], - ['basicConnection_connectionDialog_username', 'Database Username:'], - ['basicConnection_connectionDialog_usernamePlaceholder', 'user'], - ['basicConnection_connectionDialog_password', 'Database User Password:'], - ['basicConnection_connectionDialog_passwordPlaceholder', 'password'], - ['basicConnection_connectionDialog_connecting', 'Connecting...'], - ['basicConnection_connectionDialog_connecting_message', 'Connecting to database...'], - ['plugin_connection_template_action_connection_template_label', 'From a Template'], -]; diff --git a/webapp/packages/plugin-connection-template/src/locales/it.ts b/webapp/packages/plugin-connection-template/src/locales/it.ts deleted file mode 100644 index 73bcb42399..0000000000 --- a/webapp/packages/plugin-connection-template/src/locales/it.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default [ - ['basicConnection_connectionDialog_newConnection', 'Nuova connessione'], - ['basicConnection_connectionDialog_title', 'Collegati al database'], - ['basicConnection_connectionDialog_listTitle', 'Database:'], - ['basicConnection_connectionDialog_username', 'Database Username:'], - ['basicConnection_connectionDialog_usernamePlaceholder', 'utente'], - ['basicConnection_connectionDialog_password', 'Password Utente del Database:'], - ['basicConnection_connectionDialog_passwordPlaceholder', 'password'], - ['basicConnection_connectionDialog_connecting', 'Collegamento...'], - ['basicConnection_connectionDialog_connecting_message', 'Collegamento al database...'], - ['plugin_connection_template_action_connection_template_label', 'Dal Template'], -]; diff --git a/webapp/packages/plugin-connection-template/src/locales/ru.ts b/webapp/packages/plugin-connection-template/src/locales/ru.ts deleted file mode 100644 index b5e0341d86..0000000000 --- a/webapp/packages/plugin-connection-template/src/locales/ru.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default [ - ['basicConnection_connectionDialog_newConnection', 'Новое подключение'], - ['basicConnection_connectionDialog_title', 'Подключение к базе данных'], - ['basicConnection_connectionDialog_listTitle', 'Базы данных:'], - ['basicConnection_connectionDialog_username', 'Имя пользователя:'], - ['basicConnection_connectionDialog_usernamePlaceholder', 'user'], - ['basicConnection_connectionDialog_password', 'Пароль пользователя:'], - ['basicConnection_connectionDialog_passwordPlaceholder', 'password'], - ['basicConnection_connectionDialog_connecting', 'Подключение...'], - ['basicConnection_connectionDialog_connecting_message', 'Подключение к базе...'], - ['plugin_connection_template_action_connection_template_label', 'Из шаблона'], -]; diff --git a/webapp/packages/plugin-connection-template/src/locales/zh.ts b/webapp/packages/plugin-connection-template/src/locales/zh.ts deleted file mode 100644 index 0276b22b45..0000000000 --- a/webapp/packages/plugin-connection-template/src/locales/zh.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default [ - ['basicConnection_connectionDialog_newConnection', '新连接'], - ['basicConnection_connectionDialog_title', '连接数据库'], - ['basicConnection_connectionDialog_listTitle', '数据库:'], - ['basicConnection_connectionDialog_username', '用户库用户名:'], - ['basicConnection_connectionDialog_usernamePlaceholder', '用户'], - ['basicConnection_connectionDialog_password', '数据库用户密码:'], - ['basicConnection_connectionDialog_passwordPlaceholder', '密码'], - ['basicConnection_connectionDialog_connecting', '连接中...'], - ['basicConnection_connectionDialog_connecting_message', '连接数据库...'], - ['plugin_connection_template_action_connection_template_label', '从模板创建'], -]; diff --git a/webapp/packages/plugin-connection-template/src/manifest.ts b/webapp/packages/plugin-connection-template/src/manifest.ts deleted file mode 100644 index 7accde3222..0000000000 --- a/webapp/packages/plugin-connection-template/src/manifest.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 type { PluginManifest } from '@cloudbeaver/core-di'; - -import { LocaleService } from './LocaleService'; -import { TemplateConnectionPluginBootstrap } from './TemplateConnectionPluginBootstrap'; -import { TemplateConnectionsResource } from './TemplateConnectionsResource'; -import { TemplateConnectionsService } from './TemplateConnectionsService'; - -export const connectionTemplate: PluginManifest = { - info: { - name: 'Template Connections plugin', - }, - - providers: [TemplateConnectionsResource, LocaleService, TemplateConnectionPluginBootstrap, TemplateConnectionsService], -}; diff --git a/webapp/packages/plugin-connection-template/tsconfig.json b/webapp/packages/plugin-connection-template/tsconfig.json deleted file mode 100644 index 3f906c8e1b..0000000000 --- a/webapp/packages/plugin-connection-template/tsconfig.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - }, - "references": [ - { - "path": "../plugin-connections/tsconfig.json" - }, - { - "path": "../core-authentication/tsconfig.json" - }, - { - "path": "../core-blocks/tsconfig.json" - }, - { - "path": "../core-connections/tsconfig.json" - }, - { - "path": "../core-di/tsconfig.json" - }, - { - "path": "../core-dialogs/tsconfig.json" - }, - { - "path": "../core-events/tsconfig.json" - }, - { - "path": "../core-localization/tsconfig.json" - }, - { - "path": "../core-projects/tsconfig.json" - }, - { - "path": "../core-resource/tsconfig.json" - }, - { - "path": "../core-root/tsconfig.json" - }, - { - "path": "../core-sdk/tsconfig.json" - }, - { - "path": "../core-utils/tsconfig.json" - }, - { - "path": "../core-view/tsconfig.json" - } - ], - "include": [ - "__custom_mocks__/**/*", - "src/**/*", - "src/**/*.json", - "src/**/*.css", - "src/**/*.scss" - ], - "exclude": [ - "**/node_modules", - "lib/**/*", - "dist/**/*" - ] -} diff --git a/webapp/packages/plugin-connections-administration/.gitignore b/webapp/packages/plugin-connections-administration/.gitignore index 15bc16c7c3..fd03da61a3 100644 --- a/webapp/packages/plugin-connections-administration/.gitignore +++ b/webapp/packages/plugin-connections-administration/.gitignore @@ -14,4 +14,4 @@ # debug npm-debug.log* yarn-debug.log* -yarn-error.log* +yarn-error.log* \ No newline at end of file diff --git a/webapp/packages/plugin-connections-administration/package.json b/webapp/packages/plugin-connections-administration/package.json index da59c0c92d..df0878b57b 100644 --- a/webapp/packages/plugin-connections-administration/package.json +++ b/webapp/packages/plugin-connections-administration/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-connections-administration", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,37 +11,43 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-connections": "~0.1.0", - "@cloudbeaver/core-administration": "~0.1.0", - "@cloudbeaver/core-authentication": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-projects": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-administration": "workspace:*", + "@cloudbeaver/core-authentication": "workspace:*", + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-projects": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/plugin-connections": "workspace:*", + "@dbeaver/js-helpers": "workspace:^", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministration.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministration.tsx deleted file mode 100644 index 47eba59847..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministration.tsx +++ /dev/null @@ -1,124 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { ADMINISTRATION_TOOLS_PANEL_STYLES, AdministrationItemContentProps } from '@cloudbeaver/core-administration'; -import { - ColoredContainer, - Container, - Group, - GroupItem, - GroupTitle, - Loader, - ToolsAction, - ToolsPanel, - useResource, - useStyles, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import { ConnectionInfoActiveProjectKey, ConnectionInfoResource, DBDriverResource } from '@cloudbeaver/core-connections'; -import { useController, useService } from '@cloudbeaver/core-di'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; - -import { ConnectionsAdministrationController } from './ConnectionsAdministrationController'; -import { ConnectionsTable } from './ConnectionsTable/ConnectionsTable'; -import { CreateConnection } from './CreateConnection/CreateConnection'; -import { CreateConnectionService } from './CreateConnectionService'; - -const loaderStyle = css` - ExceptionMessage { - padding: 24px; - } -`; - -const styles = css` - GroupItem { - white-space: pre-wrap; - } - - ToolsPanel { - border-bottom: none; - } -`; - -export const ConnectionsAdministration = observer(function ConnectionsAdministration({ - sub, - param, - configurationWizard, -}) { - const service = useService(CreateConnectionService); - const controller = useController(ConnectionsAdministrationController); - const translate = useTranslate(); - const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES); - - useResource(ConnectionsAdministration, ConnectionInfoResource, { - key: ConnectionInfoActiveProjectKey, - includes: ['customIncludeOptions'], - }); - useResource(ConnectionsAdministration, DBDriverResource, CachedMapAllKey); - - return styled(style)( - - - - - {translate('ui_add')} - - - {translate('ui_refresh')} - - - {translate('ui_delete')} - - - - - {configurationWizard && ( - - {translate('connections_administration_configuration_wizard_title')} - {translate('connections_administration_configuration_wizard_message')} - - )} - {sub && ( - - - - )} - - - - - - - , - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationController.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationController.ts deleted file mode 100644 index 1ca2ae523a..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationController.ts +++ /dev/null @@ -1,148 +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 { computed, makeObservable, observable } from 'mobx'; - -import { ConfirmationDialogDelete } from '@cloudbeaver/core-blocks'; -import { - compareConnectionsInfo, - compareNewConnectionsInfo, - Connection, - ConnectionInfoActiveProjectKey, - ConnectionInfoResource, - createConnectionParam, - DatabaseConnection, - IConnectionInfoParams, -} from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { LocalizationService } from '@cloudbeaver/core-localization'; -import { isGlobalProject, isSharedProject, ProjectInfoResource, projectInfoSortByName } from '@cloudbeaver/core-projects'; -import { resourceKeyList } from '@cloudbeaver/core-resource'; -import { isArraysEqual, isDefined, isObjectsEqual } from '@cloudbeaver/core-utils'; - -@injectable() -export class ConnectionsAdministrationController { - isProcessing = false; - readonly selectedItems = observable(new Map()); - readonly expandedItems = observable(new Map()); - - get keys(): IConnectionInfoParams[] { - return this.connections.map(createConnectionParam); - } - - get connections(): DatabaseConnection[] { - return this.connectionInfoResource - .get(ConnectionInfoActiveProjectKey) - .filter(isDefined) - .filter(connection => { - const project = this.projectInfoResource.get(connection.projectId); - - return connection.template && project && (isSharedProject(project) || isGlobalProject(project)); - }) - .sort((connectionA, connectionB) => { - const compareNew = compareNewConnectionsInfo(connectionA, connectionB); - const projectA = this.projectInfoResource.get(connectionA.projectId); - const projectB = this.projectInfoResource.get(connectionB.projectId); - - if (compareNew !== 0) { - return compareNew; - } - - if (projectA && projectB) { - const projectSort = projectInfoSortByName(projectA, projectB); - - if (projectSort !== 0) { - return projectSort; - } - } - - return compareConnectionsInfo(connectionA, connectionB); - }); - } - - get itemsSelected(): boolean { - return Array.from(this.selectedItems.values()).some(v => v); - } - - constructor( - private readonly notificationService: NotificationService, - private readonly connectionInfoResource: ConnectionInfoResource, - private readonly commonDialogService: CommonDialogService, - private readonly localizationService: LocalizationService, - private readonly projectInfoResource: ProjectInfoResource, - ) { - makeObservable(this, { - isProcessing: observable, - connections: computed({ equals: (a, b) => isArraysEqual(a, b) }), - keys: computed({ equals: (a, b) => isArraysEqual(a, b, isObjectsEqual) }), - itemsSelected: computed, - }); - } - - update = async (): Promise => { - if (this.isProcessing) { - return; - } - this.isProcessing = true; - try { - await this.connectionInfoResource.refresh(ConnectionInfoActiveProjectKey); - this.connectionInfoResource.cleanNewFlags(); - this.notificationService.logSuccess({ title: 'connections_administration_tools_refresh_success' }); - } catch (exception: any) { - this.notificationService.logException(exception, 'connections_administration_tools_refresh_fail'); - } finally { - this.isProcessing = false; - } - }; - - delete = async (): Promise => { - if (this.isProcessing) { - return; - } - - const deletionList = Array.from(this.selectedItems) - .filter(([_, value]) => value) - .map(([connectionId]) => connectionId); - - if (deletionList.length === 0) { - return; - } - - const connectionNames = deletionList.map(id => this.connectionInfoResource.get(id)?.name).filter(Boolean); - const nameList = connectionNames.map(name => `"${name}"`).join(', '); - const message = `${this.localizationService.translate( - 'connections_administration_delete_confirmation', - )}${nameList}. ${this.localizationService.translate('ui_are_you_sure')}`; - - const result = await this.commonDialogService.open(ConfirmationDialogDelete, { - title: 'ui_data_delete_confirmation', - message, - confirmActionText: 'ui_delete', - }); - - if (result === DialogueStateResult.Rejected) { - return; - } - - this.isProcessing = true; - - try { - await this.connectionInfoResource.deleteConnection(resourceKeyList(deletionList)); - this.selectedItems.clear(); - - for (const id of deletionList) { - this.expandedItems.delete(id); - } - } catch (exception: any) { - this.notificationService.logException(exception, 'Connections delete failed'); - } finally { - this.isProcessing = false; - } - }; -} diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationNavService.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationNavService.ts deleted file mode 100644 index 7704f4a23c..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationNavService.ts +++ /dev/null @@ -1,22 +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 { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { injectable } from '@cloudbeaver/core-di'; - -@injectable() -export class ConnectionsAdministrationNavService { - constructor(private readonly administrationScreenService: AdministrationScreenService) {} - - navToRoot() { - this.administrationScreenService.navigateToItem('connections'); - } - - navToCreate(method: string) { - this.administrationScreenService.navigateToItemSub('connections', 'create', method); - } -} diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationService.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationService.ts deleted file mode 100644 index e77cebae90..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministrationService.ts +++ /dev/null @@ -1,134 +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 React from 'react'; - -import { AdministrationItemService, AdministrationItemType } from '@cloudbeaver/core-administration'; -import { ConfirmationDialog, PlaceholderContainer } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoActiveProjectKey, ConnectionInfoResource, DatabaseConnection, DBDriverResource } from '@cloudbeaver/core-connections'; -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; - -import { CreateConnectionService } from './CreateConnectionService'; - -export interface IConnectionDetailsPlaceholderProps { - connection: DatabaseConnection; -} - -const ConnectionsAdministration = React.lazy(async () => { - const { ConnectionsAdministration } = await import('./ConnectionsAdministration'); - return { default: ConnectionsAdministration }; -}); -const ConnectionsDrawerItem = React.lazy(async () => { - const { ConnectionsDrawerItem } = await import('./ConnectionsDrawerItem'); - return { default: ConnectionsDrawerItem }; -}); -const Origin = React.lazy(async () => { - const { Origin } = await import('./ConnectionsTable/ConnectionDetailsInfo/Origin'); - return { default: Origin }; -}); -const SSH = React.lazy(async () => { - const { SSH } = await import('./ConnectionsTable/ConnectionDetailsInfo/SSH'); - return { default: SSH }; -}); - -@injectable() -export class ConnectionsAdministrationService extends Bootstrap { - readonly connectionDetailsPlaceholder = new PlaceholderContainer(); - - constructor( - private readonly administrationItemService: AdministrationItemService, - private readonly notificationService: NotificationService, - private readonly connectionInfoResource: ConnectionInfoResource, - private readonly dbDriverResource: DBDriverResource, - private readonly createConnectionService: CreateConnectionService, - private readonly commonDialogService: CommonDialogService, - private readonly serverConfigResource: ServerConfigResource, - ) { - super(); - } - - register(): void { - this.administrationItemService.create({ - name: 'connections', - type: AdministrationItemType.Administration, - order: 2.2, - configurationWizardOptions: { - defaultRoute: { sub: 'create' }, - description: 'connections_administration_configuration_wizard_step_description', - }, - sub: [ - { - name: 'create', - onActivate: this.activateCreateMethod.bind(this), - onDeActivate: this.deactivateCreateMethod.bind(this), - canDeActivate: this.canDeActivateCreate.bind(this), - }, - ], - isHidden: () => this.serverConfigResource.distributed, - getContentComponent: () => ConnectionsAdministration, - getDrawerComponent: () => ConnectionsDrawerItem, - onActivate: this.loadConnections.bind(this), - onDeActivate: this.refreshUserConnections.bind(this), - }); - this.connectionDetailsPlaceholder.add(Origin, 0); - this.connectionDetailsPlaceholder.add(SSH, 2); - } - - load(): void | Promise {} - - private async refreshUserConnections(configuration: boolean, outside: boolean, outsideAdminPage: boolean): Promise { - // TODO: we have to track users' leaving the page - if (outside) { - this.connectionInfoResource.cleanNewFlags(); - // const updated = await this.connectionInfoResource.updateSessionConnections(); - - // if (updated) { - // this.sessionDataResource.markOutdated(); - // } - } - } - - private async activateCreateMethod(param: string | null) { - if (!param) { - this.createConnectionService.setCreateMethod(); - } - this.createConnectionService.setCreateMethod(param); - } - - private async deactivateCreateMethod(param: string | null, configuration: boolean, outside: boolean) { - if (outside) { - this.createConnectionService.close(); - } - } - - private async canDeActivateCreate() { - if (this.createConnectionService.data === null) { - return true; - } - - const result = await this.commonDialogService.open(ConfirmationDialog, { - title: 'ui_changes_will_be_lost', - message: 'connections_administration_deactivate_message', - confirmActionText: 'ui_continue', - }); - - return result !== DialogueStateResult.Rejected; - } - - private async loadConnections() { - try { - await this.connectionInfoResource.load(ConnectionInfoActiveProjectKey); - await this.dbDriverResource.load(CachedMapAllKey); - } catch (exception: any) { - this.notificationService.logException(exception, 'Error occurred while loading connections'); - } - } -} diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsDrawerItem.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsDrawerItem.tsx deleted file mode 100644 index 7396f283c5..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsDrawerItem.tsx +++ /dev/null @@ -1,29 +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 styled from 'reshadow'; - -import type { AdministrationItemDrawerProps } from '@cloudbeaver/core-administration'; -import { Translate, useStyles } from '@cloudbeaver/core-blocks'; -import { Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; - -export const ConnectionsDrawerItem: React.FC = function ConnectionsDrawerItem({ - item, - onSelect, - style, - disabled, - configurationWizard, -}) { - return styled(useStyles(style))( - onSelect(item.name)}> - - - - - , - ); -}; diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/Connection.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/Connection.tsx deleted file mode 100644 index df0bdd2c55..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/Connection.tsx +++ /dev/null @@ -1,77 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { Loader, Placeholder, StaticImage, TableColumnValue, TableItem, TableItemExpand, TableItemSelect } from '@cloudbeaver/core-blocks'; -import { DatabaseConnection, DBDriverResource, IConnectionInfoParams } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; - -import { ConnectionsAdministrationService } from '../ConnectionsAdministrationService'; -import { ConnectionEdit } from './ConnectionEdit'; - -const styles = css` - StaticImage { - display: flex; - width: 24px; - - &:not(:last-child) { - margin-right: 16px; - } - } - TableColumnValue[expand] { - cursor: pointer; - } - Checkbox { - margin-left: -10px; - margin-right: -10px; - } -`; - -interface Props { - connectionKey: IConnectionInfoParams; - connection: DatabaseConnection; - projectName?: string | null; -} - -export const Connection = observer(function Connection({ connectionKey, connection, projectName }) { - const driversResource = useService(DBDriverResource); - const connectionsAdministrationService = useService(ConnectionsAdministrationService); - const icon = driversResource.get(connection.driverId)?.icon; - - return styled(styles)( - - - - - - - - - - - - {connection.name} - - - {connection.host} - {connection.host && connection.port && `:${connection.port}`} - - {projectName !== undefined && ( - - {projectName} - - )} - - - - - - , - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/ConnectionDetailsStyles.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/ConnectionDetailsStyles.ts deleted file mode 100644 index 4163986891..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/ConnectionDetailsStyles.ts +++ /dev/null @@ -1,19 +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 CONNECTION_DETAILS_STYLES = css` - StaticImage { - width: 24px; - height: 24px; - margin-right: 10px; - &:last-child { - margin-right: 0; - } - } -`; diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/Origin.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/Origin.tsx deleted file mode 100644 index 3af2422328..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/Origin.tsx +++ /dev/null @@ -1,28 +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 { observer } from 'mobx-react-lite'; -import styled from 'reshadow'; - -import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; -import { PlaceholderComponent, StaticImage } from '@cloudbeaver/core-blocks'; - -import type { IConnectionDetailsPlaceholderProps } from '../../ConnectionsAdministrationService'; -import { CONNECTION_DETAILS_STYLES } from './ConnectionDetailsStyles'; - -export const Origin: PlaceholderComponent = observer(function Origin({ connection }) { - const isLocal = connection.origin?.type === AUTH_PROVIDER_LOCAL_ID; - - if (!connection.origin || isLocal) { - return null; - } - - const icon = connection.origin.icon; - const title = connection.origin.displayName; - - return styled(CONNECTION_DETAILS_STYLES)(); -}); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/SSH.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/SSH.tsx deleted file mode 100644 index 26cf7d7fcb..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/SSH.tsx +++ /dev/null @@ -1,30 +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 { observer } from 'mobx-react-lite'; -import styled from 'reshadow'; - -import { PlaceholderComponent, StaticImage, useResource, useTranslate } from '@cloudbeaver/core-blocks'; -import { NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; - -import type { IConnectionDetailsPlaceholderProps } from '../../ConnectionsAdministrationService'; -import { CONNECTION_DETAILS_STYLES } from './ConnectionDetailsStyles'; - -export const SSH: PlaceholderComponent = observer(function SSH({ connection }) { - const translate = useTranslate(); - const sshConfig = connection.networkHandlersConfig?.find(state => state.id === SSH_TUNNEL_ID); - const applicable = sshConfig?.enabled === true; - const handler = useResource(SSH, NetworkHandlerResource, SSH_TUNNEL_ID, { active: applicable }); - - if (!applicable) { - return null; - } - - return styled(CONNECTION_DETAILS_STYLES)( - , - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/Template.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/Template.tsx deleted file mode 100644 index 6493619398..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/Template.tsx +++ /dev/null @@ -1,22 +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 { observer } from 'mobx-react-lite'; -import styled from 'reshadow'; - -import { PlaceholderComponent, StaticImage } from '@cloudbeaver/core-blocks'; - -import type { IConnectionDetailsPlaceholderProps } from '../../ConnectionsAdministrationService'; -import { CONNECTION_DETAILS_STYLES } from './ConnectionDetailsStyles'; - -export const Template: PlaceholderComponent = observer(function Template({ connection }) { - if (!connection.template) { - return null; - } - - return styled(CONNECTION_DETAILS_STYLES)(); -}); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx deleted file mode 100644 index 3e511d0da9..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx +++ /dev/null @@ -1,55 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { Loader } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoResource, IConnectionInfoParams } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { ConnectionFormLoader, useConnectionFormState } from '@cloudbeaver/plugin-connections'; - -const styles = css` - box { - composes: theme-background-secondary theme-text-on-secondary from global; - box-sizing: border-box; - padding-bottom: 24px; - display: flex; - flex-direction: column; - height: 740px; - } - Loader { - height: 100%; - } -`; - -interface Props { - item: IConnectionInfoParams; -} - -export const ConnectionEdit = observer(function ConnectionEditNew({ item }) { - const connectionInfoResource = useService(ConnectionInfoResource); - // const tableContext = useContext(TableContext); - // const collapse = useCallback(() => tableContext?.setItemExpand(item, false), [tableContext, item]); - - const data = useConnectionFormState(connectionInfoResource, state => state.setOptions('edit', 'admin')); - - data.config.connectionId = item.connectionId; - data.projectId = item.projectId; - - return styled(styles)( - - - - - , - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx deleted file mode 100644 index a9946dcfa6..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx +++ /dev/null @@ -1,62 +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 { observer } from 'mobx-react-lite'; - -import { getComputed, Table, TableBody, TableColumnHeader, TableHeader, TableSelect, useResource, useTranslate } from '@cloudbeaver/core-blocks'; -import { DatabaseConnection, IConnectionInfoParams, serializeConnectionParam } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { isGlobalProject, isSharedProject, ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; - -import { Connection } from './Connection'; - -interface Props { - keys: IConnectionInfoParams[]; - connections: DatabaseConnection[]; - selectedItems: Map; - expandedItems: Map; -} - -export const ConnectionsTable = observer(function ConnectionsTable({ keys, connections, selectedItems, expandedItems }) { - const translate = useTranslate(); - const projectService = useService(ProjectsService); - const projectsLoader = useResource(ConnectionsTable, ProjectInfoResource, CachedMapAllKey); - const displayProjects = getComputed( - () => projectService.activeProjects.filter(project => isGlobalProject(project) || isSharedProject(project)).length > 1, - ); - - function getProjectName(projectId: string) { - return displayProjects ? projectsLoader.resource.get(projectId)?.name ?? null : undefined; - } - - return ( -
- - - - - - - {translate('connections_connection_name')} - {translate('connections_connection_address')} - {displayProjects && {translate('connections_connection_project')}} - - - - {connections.map((connection, i) => ( - - ))} - -
- ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnection.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnection.tsx deleted file mode 100644 index 8296562a70..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnection.tsx +++ /dev/null @@ -1,168 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { Icon, IconButton, Loader, StaticImage, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; -import { DBDriverResource } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { BASE_TAB_STYLES, TabList, TabPanelList, TabsState, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui'; -import { ConnectionFormLoader } from '@cloudbeaver/plugin-connections'; - -import { CreateConnectionService } from '../CreateConnectionService'; - -const styles = css` - title-bar { - composes: theme-border-color-background from global; - } - - connection-create { - display: flex; - flex-direction: column; - height: 800px; - overflow: hidden; - } - - connection-create-content { - composes: theme-background-secondary theme-text-on-secondary from global; - position: relative; - display: flex; - flex-direction: column; - flex: 1; - overflow: auto; - } - - Tab { - composes: theme-background-secondary theme-text-on-secondary from global; - } - - TabList { - composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global; - border-top: solid 1px; - position: relative; - flex-shrink: 0; - align-items: center; - - &:before { - content: ''; - position: absolute; - bottom: 0; - width: 100%; - border-bottom: solid 2px; - border-color: inherit; - } - } - - TabPanel, - CustomConnection, - SearchDatabase { - flex-direction: column; - height: 100%; - overflow: auto; - } - - Loader { - z-index: 1; - height: 100%; - } - - title-bar { - composes: theme-typography--headline6 from global; - padding: 16px 24px; - align-items: center; - display: flex; - font-weight: 400; - flex: auto 0 0; - } - - StaticImage { - width: 32px; - max-height: 32px; - margin-right: 16px; - } - - fill { - flex: 1; - } - - back-button { - position: relative; - box-sizing: border-box; - margin-right: 16px; - display: flex; - - & Icon { - box-sizing: border-box; - transform: rotate(90deg); - cursor: pointer; - height: 16px; - width: 16px; - } - } -`; - -const componentStyle = [BASE_TAB_STYLES, styles, UNDERLINE_TAB_STYLES]; - -interface Props { - method: string | null | undefined; - configurationWizard: boolean; -} - -export const CreateConnection = observer(function CreateConnection({ method }) { - const style = useStyles(componentStyle); - const createConnectionService = useService(CreateConnectionService); - const translate = useTranslate(); - const driver = useResource(CreateConnection, DBDriverResource, createConnectionService.data?.config.driverId || null); - - if (createConnectionService.data) { - return styled(style)( - - - - - - {driver.data?.icon && } - {driver.data?.name ?? translate('connections_administration_connection_create')} - - - - - - - - - , - ); - } - - return styled(style)( - - createConnectionService.setCreateMethod(tabId)} - > - - {translate('connections_administration_connection_create')} - - - - - - {createConnectionService.disabled && } - - - , - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap.ts deleted file mode 100644 index 0c552a45c8..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap.ts +++ /dev/null @@ -1,35 +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 React from 'react'; - -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; - -import { CreateConnectionService } from '../CreateConnectionService'; - -const CustomConnection = React.lazy(async () => { - const { CustomConnection } = await import('./Manual/CustomConnection'); - return { default: CustomConnection }; -}); - -@injectable() -export class CreateConnectionBaseBootstrap extends Bootstrap { - constructor(private readonly createConnectionService: CreateConnectionService) { - super(); - } - - register(): void | Promise { - this.createConnectionService.tabsContainer.add({ - key: 'manual', - name: 'connections_connection_create_custom', - order: 1, - panel: () => CustomConnection, - }); - } - - load(): void | Promise {} -} diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/ConnectionManualService.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/ConnectionManualService.ts deleted file mode 100644 index ef1880c768..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/ConnectionManualService.ts +++ /dev/null @@ -1,38 +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 { ConnectionInfoResource } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; - -import { CreateConnectionService } from '../../CreateConnectionService'; - -@injectable() -export class ConnectionManualService { - get disabled(): boolean { - return this.createConnectionService.disabled; - } - - set disabled(value: boolean) { - this.createConnectionService.disabled = value; - } - - constructor(private readonly connectionInfoResource: ConnectionInfoResource, private readonly createConnectionService: CreateConnectionService) { - this.select = this.select.bind(this); - } - - select(projectId: string, driverId: string): void { - this.createConnectionService.setConnectionTemplate( - projectId, - { - ...this.connectionInfoResource.getEmptyConfig(), - template: true, - driverId, - }, - [driverId], - ); - } -} diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/CustomConnection.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/CustomConnection.tsx deleted file mode 100644 index 908d8e8e6d..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/CustomConnection.tsx +++ /dev/null @@ -1,52 +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 { computed } from 'mobx'; -import { observer } from 'mobx-react-lite'; -import { useMemo } from 'react'; - -import { Loader, useResource } from '@cloudbeaver/core-blocks'; -import { DBDriverResource } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { isSharedProject, ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; - -import { ConnectionManualService } from './ConnectionManualService'; -import { DriverList } from './DriverList'; - -export const CustomConnection = observer(function CustomConnection() { - const projectsService = useService(ProjectsService); - const notificationService = useService(NotificationService); - const connectionManualService = useService(ConnectionManualService); - const dbDriverResource = useResource(CustomConnection, DBDriverResource, CachedMapAllKey); - - const drivers = useMemo( - () => computed(() => dbDriverResource.resource.enabledDrivers.slice().sort(dbDriverResource.resource.compare)), - [dbDriverResource], - ); - - useResource(CustomConnection, ProjectInfoResource, CachedMapAllKey); - - function select(driverId: string) { - const shared = projectsService.activeProjects.filter(isSharedProject); - - if (shared.length > 0) { - connectionManualService.select(shared[0].id, driverId); - } else { - notificationService.logError({ - title: 'core_projects_no_default_project', - }); - } - } - - return ( - - - - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/Driver.m.css b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/Driver.m.css deleted file mode 100644 index 4835c4cc1d..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/Driver.m.css +++ /dev/null @@ -1,5 +0,0 @@ -.staticImage { - box-sizing: border-box; - width: 24px; - max-height: 24px; -}; \ No newline at end of file diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/Driver.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/Driver.tsx deleted file mode 100644 index 064a1a76e1..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/Driver.tsx +++ /dev/null @@ -1,34 +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 { observer } from 'mobx-react-lite'; -import { useCallback } from 'react'; - -import { ListItem, ListItemDescription, ListItemIcon, ListItemName, s, StaticImage, useS } from '@cloudbeaver/core-blocks'; -import type { DBDriver } from '@cloudbeaver/core-connections'; - -import style from './Driver.m.css'; - -interface Props { - driver: DBDriver; - onSelect: (driverId: string) => void; -} - -export const Driver = observer(function Driver({ driver, onSelect }) { - const select = useCallback(() => onSelect(driver.id), [driver]); - const styles = useS(style); - - return ( - - - - - {driver.name} - {driver.description} - - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/DriverList.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/DriverList.tsx deleted file mode 100644 index 18d24ccfd6..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/DriverList.tsx +++ /dev/null @@ -1,52 +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 { observer } from 'mobx-react-lite'; -import { useMemo, useState } from 'react'; -import styled, { css } from 'reshadow'; - -import { ItemList, ItemListSearch, useFocus, useTranslate } from '@cloudbeaver/core-blocks'; -import type { DBDriver } from '@cloudbeaver/core-connections'; - -import { Driver } from './Driver'; - -interface Props { - drivers: DBDriver[]; - className?: string; - onSelect: (driverId: string) => void; -} - -const style = css` - div { - display: flex; - flex-direction: column; - overflow: auto; - } -`; - -export const DriverList = observer(function DriverList({ drivers, className, onSelect }) { - const [focusedRef] = useFocus({ focusFirstChild: true }); - const translate = useTranslate(); - const [search, setSearch] = useState(''); - const filteredDrivers = useMemo(() => { - if (!search) { - return drivers; - } - return drivers.filter(driver => driver.name?.toUpperCase().includes(search.toUpperCase())); - }, [search, drivers]); - - return styled(style)( -
- - - {filteredDrivers.map(driver => ( - - ))} - -
, - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnectionService.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnectionService.ts deleted file mode 100644 index ff6a4d846a..0000000000 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnectionService.ts +++ /dev/null @@ -1,142 +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 { action, makeObservable, observable } from 'mobx'; - -import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; -import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; -import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; -import { TabsContainer } from '@cloudbeaver/core-ui'; -import { ConnectionFormService, ConnectionFormState, IConnectionFormState } from '@cloudbeaver/plugin-connections'; - -import { ConnectionsAdministrationNavService } from './ConnectionsAdministrationNavService'; - -export interface ICreateMethodOptions { - configurationWizard?: { - activationPriority?: number; - }; - close?: () => void; -} - -@injectable() -export class CreateConnectionService { - disabled = false; - data: IConnectionFormState | null; - - readonly tabsContainer: TabsContainer; - - constructor( - private readonly connectionsAdministrationNavService: ConnectionsAdministrationNavService, - private readonly administrationScreenService: AdministrationScreenService, - private readonly connectionFormService: ConnectionFormService, - private readonly connectionInfoResource: ConnectionInfoResource, - private readonly projectsService: ProjectsService, - private readonly projectInfoResource: ProjectInfoResource, - ) { - this.data = null; - this.tabsContainer = new TabsContainer('Connection Creation mode'); - - this.setConnectionTemplate = this.setConnectionTemplate.bind(this); - this.clearConnectionTemplate = this.clearConnectionTemplate.bind(this); - this.setCreateMethod = this.setCreateMethod.bind(this); - this.cancelCreate = this.cancelCreate.bind(this); - this.create = this.create.bind(this); - - makeObservable(this, { - data: observable, - disabled: observable, - setCreateMethod: action, - cancelCreate: action, - create: action, - setConnectionTemplate: action, - clearConnectionTemplate: action, - close: action, - activateMethod: action, - }); - } - - getDefault(): string | null { - const tabs = this.tabsContainer.getDisplayed(); - - if (tabs.length === 0) { - return null; - } - - if (this.administrationScreenService.isConfigurationMode) { - const sorted = tabs.sort((a, b) => { - const aPriority = a.options?.configurationWizard?.activationPriority || Number.MAX_SAFE_INTEGER; - const bPriority = b.options?.configurationWizard?.activationPriority || Number.MAX_SAFE_INTEGER; - - return aPriority - bPriority; - }); - - return sorted[0].key; - } - - return tabs[0].key; - } - - setCreateMethod(method?: string | null): void { - if (!method) { - const id = this.getDefault(); - - if (!id) { - return; - } - method = id; - } - this.activateMethod(method); - } - - cancelCreate(): void { - this.clearConnectionTemplate(); - this.connectionsAdministrationNavService.navToRoot(); - } - - create(): void { - const defaultId = this.getDefault(); - if (!defaultId) { - return; - } - - this.activateMethod(defaultId); - } - - setConnectionTemplate(projectId: string, config: ConnectionConfig, availableDrivers: string[]): void { - this.data = new ConnectionFormState(this.projectsService, this.projectInfoResource, this.connectionFormService, this.connectionInfoResource); - - this.data.closeTask.addHandler(this.cancelCreate.bind(this)); - - this.data - .setOptions('create', 'admin') - .setConfig(projectId, config) - .setAvailableDrivers(availableDrivers || []); - - this.data.load(); - } - - clearConnectionTemplate(): void { - this.data?.dispose(); - this.data = null; - } - - close(): void { - this.clearConnectionTemplate(); - const tabs = this.tabsContainer.getDisplayed(); - - for (const method of tabs) { - method.options?.close?.(); - } - } - - private activateMethod(method: string) { - this.tabsContainer.select(method); - this.connectionsAdministrationNavService.navToCreate(method); - } -} diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx deleted file mode 100644 index 63b3a0f77b..0000000000 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx +++ /dev/null @@ -1,126 +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 { computed } from 'mobx'; -import { observer } from 'mobx-react-lite'; -import { useMemo } from 'react'; -import styled, { css } from 'reshadow'; - -import { TeamsResource, UsersResource, UsersResourceFilterKey } from '@cloudbeaver/core-authentication'; -import { - ColoredContainer, - Container, - Group, - InfoItem, - Loader, - TextPlaceholder, - useAutoLoad, - useResource, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import { isCloudConnection } from '@cloudbeaver/core-connections'; -import type { TLocalizationToken } from '@cloudbeaver/core-localization'; -import { CachedMapAllKey, CachedResourceOffsetPageListKey } from '@cloudbeaver/core-resource'; -import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '@cloudbeaver/plugin-connections'; - -import { ConnectionAccessGrantedList } from './ConnectionAccessGrantedList'; -import { ConnectionAccessList } from './ConnectionAccessList'; -import { useConnectionAccessState } from './useConnectionAccessState'; - -const styles = css` - ColoredContainer { - flex: 1; - height: 100%; - box-sizing: border-box; - } - Group { - max-height: 100%; - position: relative; - overflow: auto !important; - } - Loader { - z-index: 2; - } -`; - -export const ConnectionAccess: TabContainerPanelComponent = observer(function ConnectionAccess({ tabId, state: formState }) { - const state = useConnectionAccessState(formState.info); - const translate = useTranslate(); - - const { selected } = useTab(tabId); - - useAutoLoad(ConnectionAccess, state, selected); - - const users = useResource(ConnectionAccess, UsersResource, CachedResourceOffsetPageListKey(0, 1000).setTarget(UsersResourceFilterKey()), { - active: selected, - }); - const teams = useResource(ConnectionAccess, TeamsResource, CachedMapAllKey, { active: selected }); - - const grantedUsers = useMemo( - () => computed(() => users.resource.values.filter(user => state.state.grantedSubjects.includes(user.userId))), - [state.state.grantedSubjects, users.resource], - ); - - const grantedTeams = useMemo( - () => computed(() => teams.resource.values.filter(team => state.state.grantedSubjects.includes(team.teamId))), - [state.state.grantedSubjects, teams.resource], - ); - - if (!selected) { - return null; - } - - const loading = users.isLoading() || teams.isLoading() || state.state.loading; - const cloud = formState.info ? isCloudConnection(formState.info) : false; - const disabled = loading || !state.state.loaded || formState.disabled || cloud; - let info: TLocalizationToken | null = null; - - if (formState.mode === 'edit' && state.changed) { - info = 'ui_save_reminder'; - } else if (cloud) { - info = 'cloud_connections_access_placeholder'; - } - - return styled(styles)( - - {() => - styled(styles)( - - {!users.resource.values.length && !teams.resource.values.length ? ( - - {translate('connections_administration_connection_access_empty')} - - ) : ( - <> - {info && } - - - {state.state.editing && ( - - )} - - - )} - , - ) - } - , - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessGrantedList.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessGrantedList.tsx deleted file mode 100644 index f8503f3d4e..0000000000 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessGrantedList.tsx +++ /dev/null @@ -1,146 +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 { observable } from 'mobx'; -import { observer } from 'mobx-react-lite'; -import { useCallback, useState } from 'react'; -import styled, { css } from 'reshadow'; - -import type { TeamInfo } from '@cloudbeaver/core-authentication'; -import { - Button, - getComputed, - getSelectedItems, - Group, - Table, - TableBody, - TableColumnValue, - TableItem, - useObjectRef, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import type { TLocalizationToken } from '@cloudbeaver/core-localization'; -import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; - -import { ConnectionAccessTableHeader, IFilterState } from './ConnectionAccessTableHeader/ConnectionAccessTableHeader'; -import { ConnectionAccessTableInnerHeader } from './ConnectionAccessTableHeader/ConnectionAccessTableInnerHeader'; -import { ConnectionAccessTableItem } from './ConnectionAccessTableItem'; -import { getFilteredTeams, getFilteredUsers } from './getFilteredSubjects'; - -const styles = css` - Table { - composes: theme-background-surface theme-text-on-surface from global; - } - Group { - position: relative; - } - Group, - container, - table-container { - height: 100%; - } - container { - display: flex; - flex-direction: column; - width: 100%; - } - ConnectionAccessTableHeader { - flex: 0 0 auto; - } - table-container { - overflow: auto; - } -`; - -interface Props { - grantedUsers: AdminUserInfoFragment[]; - grantedTeams: TeamInfo[]; - disabled: boolean; - onRevoke: (subjectIds: string[]) => void; - onEdit: () => void; -} - -export const ConnectionAccessGrantedList = observer(function ConnectionAccessGrantedList({ - grantedUsers, - grantedTeams, - disabled, - onRevoke, - onEdit, -}) { - const props = useObjectRef({ onRevoke, onEdit }); - const translate = useTranslate(); - const [selectedSubjects] = useState>(() => observable(new Map())); - const [filterState] = useState(() => observable({ filterValue: '' })); - - const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); - - const revoke = useCallback(() => { - props.onRevoke(getSelectedItems(selectedSubjects)); - selectedSubjects.clear(); - }, []); - - const teams = getFilteredTeams(grantedTeams, filterState.filterValue); - const users = getFilteredUsers(grantedUsers, filterState.filterValue); - const keys = teams.map(team => team.teamId).concat(users.map(user => user.userId)); - - let tableInfoText: TLocalizationToken = 'connections_connection_access_admin_info'; - if (!keys.length) { - if (filterState.filterValue) { - tableInfoText = 'ui_search_no_result_placeholder'; - } else { - tableInfoText = 'ui_no_items_placeholder'; - } - } - - return styled(styles)( - - - - - - - - - - - - {translate(tableInfoText)} - - {teams.map(team => ( - - ))} - {users.map(user => ( - - ))} - -
-
-
-
, - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx deleted file mode 100644 index 135124cb66..0000000000 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx +++ /dev/null @@ -1,129 +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 { observable } from 'mobx'; -import { observer } from 'mobx-react-lite'; -import { useCallback, useState } from 'react'; -import styled, { css } from 'reshadow'; - -import type { TeamInfo } from '@cloudbeaver/core-authentication'; -import { - Button, - getComputed, - getSelectedItems, - Group, - Table, - TableBody, - TableColumnValue, - TableItem, - useObjectRef, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; - -import { ConnectionAccessTableHeader, IFilterState } from './ConnectionAccessTableHeader/ConnectionAccessTableHeader'; -import { ConnectionAccessTableInnerHeader } from './ConnectionAccessTableHeader/ConnectionAccessTableInnerHeader'; -import { ConnectionAccessTableItem } from './ConnectionAccessTableItem'; -import { getFilteredTeams, getFilteredUsers } from './getFilteredSubjects'; - -const styles = css` - Table { - composes: theme-background-surface theme-text-on-surface from global; - } - Group { - position: relative; - } - Group, - container, - table-container { - height: 100%; - } - container { - display: flex; - flex-direction: column; - width: 100%; - } - table-container { - overflow: auto; - } - ConnectionAccessTableHeader { - flex: 0 0 auto; - } -`; - -interface Props { - userList: AdminUserInfoFragment[]; - teamList: TeamInfo[]; - grantedSubjects: string[]; - onGrant: (subjectIds: string[]) => void; - disabled: boolean; -} - -export const ConnectionAccessList = observer(function ConnectionAccessList({ userList, teamList, grantedSubjects, onGrant, disabled }) { - const props = useObjectRef({ onGrant }); - const translate = useTranslate(); - const [selectedSubjects] = useState>(() => observable(new Map())); - const [filterState] = useState(() => observable({ filterValue: '' })); - - const teams = getFilteredTeams(teamList, filterState.filterValue); - const users = getFilteredUsers(userList, filterState.filterValue); - const keys = teams.map(team => team.teamId).concat(users.map(user => user.userId)); - - const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); - - const grant = useCallback(() => { - props.onGrant(getSelectedItems(selectedSubjects)); - selectedSubjects.clear(); - }, []); - - return styled(styles)( - - - - - - - !grantedSubjects.includes(item)}> - - - {!keys.length && filterState.filterValue && ( - - {translate('ui_search_no_result_placeholder')} - - )} - {teams.map(team => ( - - ))} - {users.map(user => ( - - ))} - -
-
-
-
, - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts deleted file mode 100644 index bb1371c0a1..0000000000 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts +++ /dev/null @@ -1,141 +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 React from 'react'; - -import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { EAdminPermission } from '@cloudbeaver/core-authentication'; -import { ConnectionInfoResource, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections'; -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { executorHandlerFilter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { PermissionsService } from '@cloudbeaver/core-root'; -import { formStateContext } from '@cloudbeaver/core-ui'; -import type { MetadataValueGetter } from '@cloudbeaver/core-utils'; -import { - connectionConfigContext, - ConnectionFormService, - IConnectionFormProps, - IConnectionFormState, - IConnectionFormSubmitData, -} from '@cloudbeaver/plugin-connections'; - -import type { IConnectionAccessTabState } from './IConnectionAccessTabState'; - -const ConnectionAccess = React.lazy(async () => { - const { ConnectionAccess } = await import('./ConnectionAccess'); - return { default: ConnectionAccess }; -}); - -@injectable() -export class ConnectionAccessTabService extends Bootstrap { - private readonly key: string; - - constructor( - private readonly connectionFormService: ConnectionFormService, - private readonly administrationScreenService: AdministrationScreenService, - private readonly connectionInfoResource: ConnectionInfoResource, - private readonly permissionsResource: PermissionsService, - private readonly projectInfoResource: ProjectInfoResource, - ) { - super(); - this.key = 'access'; - } - - register(): void { - this.connectionFormService.tabsContainer.add({ - key: this.key, - name: 'connections_connection_edit_access', - title: 'connections_connection_edit_access', - order: 4, - stateGetter: context => this.stateGetter(context), - isHidden: (_, context) => !context || !this.isAccessTabActive(context.state), - isDisabled: (tabId, props) => !props?.state.config.driverId || this.administrationScreenService.isConfigurationMode, - panel: () => ConnectionAccess, - }); - - this.connectionFormService.formSubmittingTask.addHandler(executorHandlerFilter(data => this.isAccessTabActive(data.state), this.save.bind(this))); - - this.connectionFormService.formStateTask.addHandler(executorHandlerFilter(this.isAccessTabActive.bind(this), this.formState.bind(this))); - } - - load(): void {} - - private isAccessTabActive(state: IConnectionFormState): boolean { - return ( - state.projectId !== null && - isGlobalProject(this.projectInfoResource.get(state.projectId)) && - this.permissionsResource.has(EAdminPermission.admin) - ); - } - - private stateGetter(context: IConnectionFormProps): MetadataValueGetter { - return () => ({ - loading: false, - loaded: false, - editing: false, - grantedSubjects: [], - initialGrantedSubjects: [], - }); - } - - private async save(data: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - if (data.submitType === 'test' || !data.state.projectId) { - return; - } - const status = contexts.getContext(this.connectionFormService.connectionStatusContext); - - if (!status.saved) { - return; - } - - const config = contexts.getContext(connectionConfigContext); - const state = this.connectionFormService.tabsContainer.getTabState(data.state.partsState, this.key, { - state: data.state, - }); - - if (!config.connectionId || !state.loaded) { - return; - } - - const key = createConnectionParam(data.state.projectId, config.connectionId); - - const changed = await this.isChanged(key, state.grantedSubjects); - - if (changed) { - await this.connectionInfoResource.setAccessSubjects(key, state.grantedSubjects); - state.initialGrantedSubjects = state.grantedSubjects.slice(); - } - } - - private async formState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - const state = this.connectionFormService.tabsContainer.getTabState(data.partsState, this.key, { state: data }); - - if (!config.connectionId || !data.projectId || !state.loaded) { - return; - } - - const key = createConnectionParam(data.projectId, config.connectionId); - const changed = await this.isChanged(key, state.grantedSubjects); - - if (changed) { - const stateContext = contexts.getContext(formStateContext); - - stateContext.markEdited(); - } - } - - private async isChanged(connectionKey: IConnectionInfoParams, next: string[]): Promise { - const current = await this.connectionInfoResource.loadAccessSubjects(connectionKey); - if (current.length !== next.length) { - return true; - } - - return current.some(value => !next.some(subjectId => subjectId === value.subjectId)); - } -} diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableHeader/ConnectionAccessTableHeader.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableHeader/ConnectionAccessTableHeader.tsx deleted file mode 100644 index 2e0590a279..0000000000 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableHeader/ConnectionAccessTableHeader.tsx +++ /dev/null @@ -1,61 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { Filter, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; - -export interface IFilterState { - filterValue: string; -} - -interface Props { - filterState: IFilterState; - disabled: boolean; - className?: string; -} - -const styles = css` - buttons { - display: flex; - gap: 16px; - } - header { - composes: theme-border-color-background theme-background-surface theme-text-on-surface from global; - overflow: hidden; - position: sticky; - top: 0; - z-index: 1; - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px; - gap: 16px; - border-bottom: 1px solid; - } -`; - -export const ConnectionAccessTableHeader = observer>(function ConnectionAccessTableHeader({ - filterState, - disabled, - className, - children, -}) { - const translate = useTranslate(); - return styled(useStyles(styles))( -
- - {children} -
, - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableHeader/ConnectionAccessTableInnerHeader.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableHeader/ConnectionAccessTableInnerHeader.tsx deleted file mode 100644 index 687681ceeb..0000000000 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableHeader/ConnectionAccessTableInnerHeader.tsx +++ /dev/null @@ -1,29 +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 { observer } from 'mobx-react-lite'; - -import { TableColumnHeader, TableHeader, TableSelect, useTranslate } from '@cloudbeaver/core-blocks'; - -interface Props { - disabled?: boolean; - className?: string; -} - -export const ConnectionAccessTableInnerHeader = observer(function ConnectionAccessTableInnerHeader({ disabled, className }) { - const translate = useTranslate(); - return ( - - - - - - {translate('connections_connection_access_user_or_team_name')} - {translate('connections_connection_description')} - - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableItem.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableItem.tsx deleted file mode 100644 index 1c48ef168f..0000000000 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableItem.tsx +++ /dev/null @@ -1,53 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { StaticImage, TableColumnValue, TableItem, TableItemSelect } from '@cloudbeaver/core-blocks'; - -interface Props { - id: any; - name: string; - icon: string; - disabled: boolean; - iconTooltip?: string; - tooltip?: string; - description?: string; - className?: string; -} - -const style = css` - StaticImage { - display: flex; - width: 24px; - } -`; - -export const ConnectionAccessTableItem = observer(function ConnectionAccessTableItem({ - id, - name, - description, - icon, - iconTooltip, - tooltip, - disabled, - className, -}) { - return styled(style)( - - - - - - - - {name} - {description} - , - ); -}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/IConnectionAccessTabState.ts b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/IConnectionAccessTabState.ts deleted file mode 100644 index 8651da506c..0000000000 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/IConnectionAccessTabState.ts +++ /dev/null @@ -1,15 +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. - */ - -export interface IConnectionAccessTabState { - loading: boolean; - loaded: boolean; - grantedSubjects: string[]; - initialGrantedSubjects: string[]; - editing: boolean; -} diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts deleted file mode 100644 index e99b6bc394..0000000000 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts +++ /dev/null @@ -1,106 +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 { action, computed, observable } from 'mobx'; - -import { IAutoLoadable, useObservableRef } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import type { DatabaseConnectionFragment } from '@cloudbeaver/core-sdk'; -import { useTabState } from '@cloudbeaver/core-ui'; -import { isArraysEqual, isContainsException } from '@cloudbeaver/core-utils'; - -import type { IConnectionAccessTabState } from './IConnectionAccessTabState'; - -interface State extends IAutoLoadable { - state: IConnectionAccessTabState; - changed: boolean; - edit: () => void; - revoke: (subjectIds: string[]) => void; - grant: (subjectIds: string[]) => void; - load: () => Promise; -} - -export function useConnectionAccessState(connection: DatabaseConnectionFragment | undefined): Readonly { - const resource = useService(ConnectionInfoResource); - const notificationService = useService(NotificationService); - const state = useTabState(); - - return useObservableRef( - () => ({ - exception: null as Error | null, - get changed() { - return !isArraysEqual(this.state.initialGrantedSubjects, this.state.grantedSubjects); - }, - isLoading() { - return this.state.loading; - }, - isError() { - return isContainsException(this.exception); - }, - isLoaded() { - return this.state.loaded; - }, - edit() { - this.state.editing = !this.state.editing; - }, - revoke(subjectIds: string[]) { - this.state.grantedSubjects = this.state.grantedSubjects.filter(subject => !subjectIds.includes(subject)); - }, - grant(subjectIds: string[]) { - this.state.grantedSubjects.push(...subjectIds); - }, - async load(reload = false) { - let loaded = this.exception || this.state.loaded; - - if (reload) { - loaded = false; - } - - if (loaded || this.state.loading) { - return; - } - - try { - this.state.loading = true; - - if (this.connection) { - const key = createConnectionParam(this.connection); - const grantedSubjects = await this.resource.loadAccessSubjects(key); - this.state.grantedSubjects = grantedSubjects.map(subject => subject.subjectId); - this.state.initialGrantedSubjects = this.state.grantedSubjects.slice(); - } - - this.state.loaded = true; - this.exception = null; - } catch (exception: any) { - this.notificationService.logException(exception, 'connections_connection_edit_access_load_failed'); - this.exception = exception; - } finally { - this.state.loading = false; - } - }, - async reload() { - this.load(true); - }, - }), - { - exception: observable.ref, - state: observable.ref, - changed: computed, - edit: action.bound, - isLoading: action.bound, - isLoaded: action.bound, - reload: action.bound, - revoke: action.bound, - grant: action.bound, - }, - { state, connection, resource, notificationService }, - ['load'], - ); -} diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccess.module.css b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccess.module.css new file mode 100644 index 0000000000..dc22308ae2 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccess.module.css @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.coloredContainer { + flex: 1; + height: 100%; + box-sizing: border-box; +} + +.group { + max-height: 100%; + position: relative; + overflow: auto !important; +} + +.loader { + z-index: 2; +} diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccess.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccess.tsx new file mode 100644 index 0000000000..2b3fa31f69 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccess.tsx @@ -0,0 +1,138 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useMemo, useState } from 'react'; + +import { TeamsResource, UsersResource, UsersResourceFilterKey } from '@cloudbeaver/core-authentication'; +import { + ColoredContainer, + Container, + Group, + InfoItem, + Loader, + s, + TextPlaceholder, + useAutoLoad, + useObservableRef, + useResource, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { ConnectionInfoOriginResource, ConnectionInfoResource, createConnectionParam, isCloudConnection } from '@cloudbeaver/core-connections'; +import type { TLocalizationToken } from '@cloudbeaver/core-localization'; +import { CachedMapAllKey, CachedResourceOffsetPageListKey } from '@cloudbeaver/core-resource'; +import { FormMode, type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; +import { getConnectionFormOptionsPart, type IConnectionFormProps } from '@cloudbeaver/plugin-connections'; + +import styles from './ConnectionFormAccess.module.css'; +import { getConnectionFormAccessPart } from './getConnectionFormAccessPart.js'; +import { ConnectionFormAccessTableGrantedList } from './ConnectionFormAccessTable/ConnectionFormAccessTableGrantedList.js'; +import { ConnectionFormAccessTableList } from './ConnectionFormAccessTable/ConnectionFormAccessTableList.js'; + +export const ConnectionFormAccess: TabContainerPanelComponent = observer(function ConnectionFormAccess({ tabId, formState }) { + const translate = useTranslate(); + const style = useS(styles); + const state = useObservableRef( + () => ({ + editing: false, + toggleEdit() { + this.editing = !this.editing; + }, + }), + { + editing: observable.ref, + toggleEdit: action.bound, + }, + false, + ); + + const { selected } = useTab(tabId); + const accessPart = getConnectionFormAccessPart(formState); + const [initialFormMode] = useState(formState.mode); + + useAutoLoad(ConnectionFormAccess, accessPart, selected); + + const users = useResource(ConnectionFormAccess, UsersResource, CachedResourceOffsetPageListKey(0, 1000).setParent(UsersResourceFilterKey()), { + active: selected, + }); + const teams = useResource(ConnectionFormAccess, TeamsResource, CachedMapAllKey, { active: selected }); + + const grantedUsers = useMemo( + () => computed(() => users.resource.values.filter(user => accessPart.state.includes(user.userId))), + [accessPart.state, users.resource], + ); + + const grantedTeams = useMemo( + () => computed(() => teams.resource.values.filter(team => accessPart.state.includes(team.teamId))), + [accessPart.state, teams.resource], + ); + + const optionsPart = getConnectionFormOptionsPart(formState); + const connectionParam = + optionsPart.state.connectionId !== undefined ? createConnectionParam(formState.state.projectId, optionsPart.state.connectionId) : null; + const connectionInfoResource = useResource(ConnectionFormAccess, ConnectionInfoResource, connectionParam, { + active: selected, + }); + const originInfoResource = useResource(ConnectionFormAccess, ConnectionInfoOriginResource, connectionParam, { + active: selected, + }); + const connectionInfo = connectionInfoResource.data; + const originInfo = originInfoResource.data; + const loading = users.isLoading() || teams.isLoading() || accessPart.isLoading(); + const cloud = connectionInfo && originInfo?.origin ? isCloudConnection(originInfo.origin) : false; + const disabled = loading || !accessPart.isLoaded() || formState.isDisabled || cloud; + let info: TLocalizationToken | null = null; + + if (initialFormMode === FormMode.Edit && accessPart.isChanged) { + info = 'ui_save_reminder'; + } else if (cloud) { + info = 'cloud_connections_access_placeholder'; + } + + if (!selected) { + return null; + } + + return ( + + {() => ( + + {!users.resource.values.length && !teams.resource.values.length ? ( + + {translate('core_connections_connection_access_empty')} + + ) : ( + <> + {info && } + + + {state.editing && ( + + )} + + + )} + + )} + + ); +}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessPart.ts b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessPart.ts new file mode 100644 index 0000000000..9dacaf7dd3 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessPart.ts @@ -0,0 +1,80 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { FormPart, formSubmitContext, type IFormState } from '@cloudbeaver/core-ui'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { type ConnectionInfoResource } from '@cloudbeaver/core-connections'; +import { action, makeObservable } from 'mobx'; +import { type IConnectionFormState, ConnectionFormOptionsPart } from '@cloudbeaver/plugin-connections'; +import { getSubjectDifferences } from '@cloudbeaver/core-utils'; + +function getDefaultState(): string[] { + return []; +} + +export class ConnectionFormAccessPart extends FormPart { + constructor( + formState: IFormState, + private readonly connectionInfoResource: ConnectionInfoResource, + private readonly optionsPart: ConnectionFormOptionsPart, + ) { + super(formState, getDefaultState()); + + makeObservable(this, { + revoke: action.bound, + grant: action.bound, + }); + } + + override isOutdated(): boolean { + return Boolean(this.optionsPart.connectionKey && this.connectionInfoResource.isOutdated(this.optionsPart.connectionKey)); + } + + protected override async loader(): Promise { + if (!this.optionsPart.connectionKey) { + this.setInitialState(getDefaultState()); + return; + } + + await this.connectionInfoResource.load(this.optionsPart.connectionKey); + const subjects = await this.connectionInfoResource.loadAccessSubjects(this.optionsPart.connectionKey); + + this.setInitialState(subjects.map(subject => subject.subjectId)); + } + + protected override async saveChanges( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise { + const submitInfo = contexts.getContext(formSubmitContext); + if (submitInfo.type === 'test' || !this.optionsPart.connectionKey) { + return; + } + + const { subjectsToRevoke, subjectsToGrant } = getSubjectDifferences(this.initialState, this.state); + + const promises = []; + + if (subjectsToRevoke.length > 0) { + promises.push(this.connectionInfoResource.deleteConnectionsAccess(this.optionsPart.connectionKey, subjectsToRevoke)); + } + + if (subjectsToGrant.length > 0) { + promises.push(this.connectionInfoResource.addConnectionsAccess(this.optionsPart.connectionKey, subjectsToGrant)); + } + + await Promise.all(promises); + } + + revoke(subjectIds: string[]) { + this.state = this.state.filter(subject => !subjectIds.includes(subject)); + } + + grant(subjectIds: string[]) { + this.state.push(...subjectIds); + } +} \ No newline at end of file diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTabBootstrap.ts b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTabBootstrap.ts new file mode 100644 index 0000000000..cb07666e1e --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTabBootstrap.ts @@ -0,0 +1,59 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { AdministrationScreenService } from '@cloudbeaver/core-administration'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { EAdminPermission, PermissionsService, SessionPermissionsResource } from '@cloudbeaver/core-root'; +import { ConnectionFormService, getConnectionFormOptionsPart } from '@cloudbeaver/plugin-connections'; +import { getConnectionFormAccessPart } from './getConnectionFormAccessPart.js'; +import { getCachedDataResourceLoaderState, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +const ConnectionFormAccess = importLazyComponent(() => import('./ConnectionFormAccess.js').then(m => m.ConnectionFormAccess)); + +@injectable(() => [ConnectionFormService, AdministrationScreenService, SessionPermissionsResource, PermissionsService, ProjectInfoResource]) +export class ConnectionFormAccessTabBootstrap extends Bootstrap { + private readonly key: string; + + constructor( + private readonly ConnectionFormService: ConnectionFormService, + private readonly administrationScreenService: AdministrationScreenService, + private readonly sessionPermissionsResource: SessionPermissionsResource, + private readonly permissionsResource: PermissionsService, + private readonly projectInfoResource: ProjectInfoResource, + ) { + super(); + this.key = 'access'; + } + + override register(): void { + this.ConnectionFormService.parts.add({ + key: this.key, + name: 'connections_connection_edit_access', + title: 'connections_connection_edit_access', + order: 4, + stateGetter: context => () => getConnectionFormAccessPart(context.formState), + getLoader: (_, context) => [ + getCachedMapResourceLoaderState(this.projectInfoResource, () => context?.formState.state.projectId ?? null), + getCachedDataResourceLoaderState(this.sessionPermissionsResource, () => undefined), + ], + isHidden: (_, context) => !context || !this.isAccessTabActive(context.formState.state.projectId), + isDisabled: (tabId, props) => { + const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null; + + return !optionsPart?.state.driverId || this.administrationScreenService.isConfigurationMode; + }, + panel: () => ConnectionFormAccess, + }); + } + + private isAccessTabActive(projectId: string | null): boolean { + return projectId !== null && isGlobalProject(this.projectInfoResource.get(projectId)) && this.permissionsResource.has(EAdminPermission.admin); + } +} diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableGrantedList.module.css b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableGrantedList.module.css new file mode 100644 index 0000000000..2bc0039ccf --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableGrantedList.module.css @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.table { + composes: theme-background-surface theme-text-on-surface from global; +} + +.group { + position: relative; +} + +.group, +.container, +.tableContainer { + height: 100%; +} + +.container { + display: flex; + flex-direction: column; + width: 100%; +} + +.connectionAccessTableHeader { + flex: 0 0 auto; +} + +.tableContainer { + overflow: auto; +} diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableGrantedList.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableGrantedList.tsx new file mode 100644 index 0000000000..ddcd65381a --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableGrantedList.tsx @@ -0,0 +1,124 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useState } from 'react'; + +import type { TeamInfo } from '@cloudbeaver/core-authentication'; +import { + Button, + getComputed, + getSelectedItems, + Group, + s, + Table, + TableBody, + TableColumnValue, + TableItem, + useObjectRef, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import type { TLocalizationToken } from '@cloudbeaver/core-localization'; +import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; + +import styles from './ConnectionFormAccessTableGrantedList.module.css'; +import { getFilteredUsers, getFilteredTeams } from '../getFilteredSubjects.js'; +import { ConnectionFormAccessTableHeader, type IFilterState } from './ConnectionFormAccessTableHeader/ConnectionFormAccessTableHeader.js'; +import { ConnectionFormAccessTableInnerHeader } from './ConnectionFormAccessTableHeader/ConnectionFormAccessTableInnerHeader.js'; +import { ConnectionFormAccessTableItem } from './ConnectionFormAccessTableItem.js'; + +interface Props { + grantedUsers: AdminUserInfoFragment[]; + grantedTeams: TeamInfo[]; + disabled: boolean; + onRevoke: (subjectIds: string[]) => void; + onEdit: () => void; +} + +export const ConnectionFormAccessTableGrantedList = observer(function ConnectionFormAccessTableGrantedList({ + grantedUsers, + grantedTeams, + disabled, + onRevoke, + onEdit, +}) { + const props = useObjectRef({ onRevoke, onEdit }); + const translate = useTranslate(); + const style = useS(styles); + const [selectedSubjects] = useState>(() => observable(new Map())); + const [filterState] = useState(() => observable({ filterValue: '' })); + + const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); + + const revoke = useCallback(() => { + props.onRevoke(getSelectedItems(selectedSubjects)); + selectedSubjects.clear(); + }, []); + + const teams = getFilteredTeams(grantedTeams, filterState.filterValue); + const users = getFilteredUsers(grantedUsers, filterState.filterValue); + const keys = grantedTeams.map(team => team.teamId).concat(grantedUsers.map(user => user.userId)); + + let tableInfoText: TLocalizationToken = 'connections_connection_access_admin_info'; + if (!keys.length) { + if (filterState.filterValue) { + tableInfoText = 'ui_search_no_result_placeholder'; + } else { + tableInfoText = 'ui_no_items_placeholder'; + } + } + + return ( + +
+ + + + +
+ + + + + {translate(tableInfoText)} + + {teams.map(team => ( + + ))} + {users.map(user => ( + + ))} + +
+
+
+
+ ); +}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableHeader/ConnectionFormAccessTableHeader.module.css b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableHeader/ConnectionFormAccessTableHeader.module.css new file mode 100644 index 0000000000..0f086c1485 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableHeader/ConnectionFormAccessTableHeader.module.css @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.buttons { + display: flex; + gap: 16px; +} + +.header { + composes: theme-border-color-background theme-background-surface theme-text-on-surface from global; + overflow: hidden; + position: sticky; + top: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + gap: 16px; + border-bottom: 1px solid; +} diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableHeader/ConnectionFormAccessTableHeader.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableHeader/ConnectionFormAccessTableHeader.tsx new file mode 100644 index 0000000000..9e9fd33313 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableHeader/ConnectionFormAccessTableHeader.tsx @@ -0,0 +1,43 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Filter, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; + +import styles from './ConnectionFormAccessTableHeader.module.css'; + +export interface IFilterState { + filterValue: string; +} + +interface Props { + filterState: IFilterState; + disabled: boolean; + className?: string; +} + +export const ConnectionFormAccessTableHeader = observer>(function ConnectionFormAccessTableHeader({ + filterState, + disabled, + className, + children, +}) { + const translate = useTranslate(); + const style = useS(styles); + return ( +
+ +
{children}
+
+ ); +}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableHeader/ConnectionFormAccessTableInnerHeader.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableHeader/ConnectionFormAccessTableInnerHeader.tsx new file mode 100644 index 0000000000..0f813d22d6 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableHeader/ConnectionFormAccessTableInnerHeader.tsx @@ -0,0 +1,29 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { TableColumnHeader, TableHeader, TableSelect, useTranslate } from '@cloudbeaver/core-blocks'; + +interface Props { + disabled?: boolean; + className?: string; +} + +export const ConnectionFormAccessTableInnerHeader = observer(function ConnectionFormAccessTableInnerHeader({ disabled, className }) { + const translate = useTranslate(); + return ( + + + + + + {translate('connections_connection_access_user_or_team_name')} + {translate('connections_connection_description')} + + ); +}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableItem.module.css b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableItem.module.css new file mode 100644 index 0000000000..a5d7bce80a --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableItem.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.staticImage { + display: flex; + width: 24px; +} diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableItem.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableItem.tsx new file mode 100644 index 0000000000..d0f5310ba3 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableItem.tsx @@ -0,0 +1,48 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { s, StaticImage, TableColumnValue, TableItem, TableItemSelect, useS } from '@cloudbeaver/core-blocks'; + +import styles from './ConnectionFormAccessTableItem.module.css'; + +interface Props { + id: any; + name: string; + icon: string; + disabled: boolean; + iconTooltip?: string; + tooltip?: string; + description?: string; + className?: string; +} + +export const ConnectionFormAccessTableItem = observer(function ConnectionFormAccessTableItem({ + id, + name, + description, + icon, + iconTooltip, + tooltip, + disabled, + className, +}) { + const style = useS(styles); + return ( + + + + + + + + {name} + {description} + + ); +}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableList.module.css b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableList.module.css new file mode 100644 index 0000000000..ff0ef19f31 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableList.module.css @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.table { + composes: theme-background-surface theme-text-on-surface from global; +} + +.group { + position: relative; +} + +.group, +.container, +.tableContainer { + height: 100%; +} + +.container { + display: flex; + flex-direction: column; + width: 100%; +} + +.tableContainer { + overflow: auto; +} + +.connectionAccessTableHeader { + flex: 0 0 auto; +} diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableList.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableList.tsx new file mode 100644 index 0000000000..845b3f12d9 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/ConnectionFormAccessTable/ConnectionFormAccessTableList.tsx @@ -0,0 +1,118 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useState } from 'react'; + +import type { TeamInfo } from '@cloudbeaver/core-authentication'; +import { + Button, + getComputed, + getSelectedItems, + Group, + s, + Table, + TableBody, + TableColumnValue, + TableItem, + useObjectRef, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; + +import styles from './ConnectionFormAccessTableList.module.css'; +import { getFilteredTeams, getFilteredUsers } from '../getFilteredSubjects.js'; +import { ConnectionFormAccessTableHeader, type IFilterState } from './ConnectionFormAccessTableHeader/ConnectionFormAccessTableHeader.js'; +import { ConnectionFormAccessTableInnerHeader } from './ConnectionFormAccessTableHeader/ConnectionFormAccessTableInnerHeader.js'; +import { ConnectionFormAccessTableItem } from './ConnectionFormAccessTableItem.js'; + +interface Props { + userList: AdminUserInfoFragment[]; + teamList: TeamInfo[]; + grantedSubjects: string[]; + onGrant: (subjectIds: string[]) => void; + disabled: boolean; +} + +export const ConnectionFormAccessTableList = observer(function ConnectionFormAccessTableList({ + userList, + teamList, + grantedSubjects, + onGrant, + disabled, +}) { + const props = useObjectRef({ onGrant }); + const translate = useTranslate(); + const style = useS(styles); + const [selectedSubjects] = useState>(() => observable(new Map())); + const [filterState] = useState(() => observable({ filterValue: '' })); + + const teams = getFilteredTeams(teamList, filterState.filterValue); + const users = getFilteredUsers(userList, filterState.filterValue); + const keys = teamList.map(team => team.teamId).concat(userList.map(user => user.userId)); + + const selected = getComputed(() => Array.from(selectedSubjects.values()).some(v => v)); + + const grant = useCallback(() => { + props.onGrant(getSelectedItems(selectedSubjects)); + selectedSubjects.clear(); + }, []); + + return ( + +
+ + + +
+ !grantedSubjects.includes(item)} + > + + + {!keys.length && filterState.filterValue && ( + + {translate('ui_search_no_result_placeholder')} + + )} + {teams.map(team => ( + + ))} + {users.map(user => ( + + ))} + +
+
+
+
+ ); +}); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/IConnectionFormAccessTabState.ts b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/IConnectionFormAccessTabState.ts new file mode 100644 index 0000000000..d442e5afc2 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/IConnectionFormAccessTabState.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 IConnectionFormAccessTabState { + loading: boolean; + loaded: boolean; + grantedSubjects: string[]; + initialGrantedSubjects: string[]; + editing: boolean; +} \ No newline at end of file diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/getConnectionFormAccessPart.ts b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/getConnectionFormAccessPart.ts new file mode 100644 index 0000000000..febb0e86e5 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/getConnectionFormAccessPart.ts @@ -0,0 +1,24 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import type { IFormState } from '@cloudbeaver/core-ui'; +import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; +import { ConnectionFormAccessPart } from './ConnectionFormAccessPart.js'; +import { getConnectionFormOptionsPart, type IConnectionFormState } from '@cloudbeaver/plugin-connections'; + +const DATA_CONTEXT_CONNECTION_FORM_ACCESS_PART = createDataContext('Connection Form Access Part'); + +export function getConnectionFormAccessPart(formState: IFormState): ConnectionFormAccessPart { + return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_ACCESS_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const connectionInfoResource = di.getService(ConnectionInfoResource); + const optionsPart = getConnectionFormOptionsPart(formState); + + return new ConnectionFormAccessPart(formState, connectionInfoResource, optionsPart); + }); +} \ No newline at end of file diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/getFilteredSubjects.ts b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/getFilteredSubjects.ts similarity index 95% rename from webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/getFilteredSubjects.ts rename to webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/getFilteredSubjects.ts index 6232fd4120..dc6bce0dd7 100644 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/getFilteredSubjects.ts +++ b/webapp/packages/plugin-connections-administration/src/ConnectionFormAccess/getFilteredSubjects.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections-administration/src/LocaleService.ts b/webapp/packages/plugin-connections-administration/src/LocaleService.ts deleted file mode 100644 index e3649a06b4..0000000000 --- a/webapp/packages/plugin-connections-administration/src/LocaleService.ts +++ /dev/null @@ -1,35 +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 { 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/plugin-connections-administration/src/index.ts b/webapp/packages/plugin-connections-administration/src/index.ts index 939e1ee7f7..4cb153a0b1 100644 --- a/webapp/packages/plugin-connections-administration/src/index.ts +++ b/webapp/packages/plugin-connections-administration/src/index.ts @@ -1,11 +1,12 @@ -import { connectionPlugin } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 * from './Administration/Connections/ConnectionsAdministration'; -export * from './Administration/Connections/CreateConnection/Manual/ConnectionManualService'; -export * from './Administration/Connections/ConnectionsAdministrationNavService'; -export * from './Administration/Connections/ConnectionsAdministrationService'; -export * from './Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap'; -export * from './Administration/Connections/CreateConnectionService'; -export * from './ConnectionForm/ConnectionAccess/ConnectionAccessTabService'; +import './module.js'; +import { connectionPlugin } from './manifest.js'; export default connectionPlugin; diff --git a/webapp/packages/plugin-connections-administration/src/locales/en.ts b/webapp/packages/plugin-connections-administration/src/locales/en.ts deleted file mode 100644 index 23bd846516..0000000000 --- a/webapp/packages/plugin-connections-administration/src/locales/en.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default [ - ['connections_public_connection_edit_menu_item_title', 'Edit Connection'], - ['connections_public_connection_edit_cancel_title', 'Cancel confirmation'], - ['connections_public_connection_edit_reconnect_title', 'Connection updated'], - ['connections_public_connection_edit_reconnect_message', 'Connection has been updated. Do you want to reconnect?'], - ['connections_public_connection_edit_reconnect_failed', 'Failed to reconnect'], - - ['connections_administration_deactivate_message', "Your connection's settings will be lost. Do you want to continue?"], -]; diff --git a/webapp/packages/plugin-connections-administration/src/locales/it.ts b/webapp/packages/plugin-connections-administration/src/locales/it.ts deleted file mode 100644 index 3be43aa970..0000000000 --- a/webapp/packages/plugin-connections-administration/src/locales/it.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default [ - ['connections_public_connection_edit_menu_item_title', 'Modifica Connessione'], - ['connections_public_connection_edit_cancel_title', "Conferma l'annullamento"], -]; diff --git a/webapp/packages/plugin-connections-administration/src/locales/ru.ts b/webapp/packages/plugin-connections-administration/src/locales/ru.ts deleted file mode 100644 index fddccfa941..0000000000 --- a/webapp/packages/plugin-connections-administration/src/locales/ru.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default [ - ['connections_public_connection_edit_menu_item_title', 'Изменить подключение'], - ['connections_public_connection_edit_cancel_title', 'Отмена редактирования'], - ['connections_public_connection_edit_reconnect_title', 'Подключение обновлено'], - ['connections_public_connection_edit_reconnect_message', 'Подключение было обновлено. Вы хотите переподключиться?'], - ['connections_public_connection_edit_reconnect_failed', 'Не удалось переподключиться'], - - ['connections_administration_deactivate_message', 'Настройки вашего подключения будут потеряны. Вы хотите продолжить?'], -]; diff --git a/webapp/packages/plugin-connections-administration/src/locales/zh.ts b/webapp/packages/plugin-connections-administration/src/locales/zh.ts deleted file mode 100644 index e73b0ed1e4..0000000000 --- a/webapp/packages/plugin-connections-administration/src/locales/zh.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default [ - ['connections_public_connection_edit_menu_item_title', '编辑连接'], - ['connections_public_connection_edit_cancel_title', '取消确认'], - ['connections_public_connection_edit_reconnect_title', '连接已更新'], - ['connections_public_connection_edit_reconnect_message', '连接已更新。您想重新连接吗?'], - ['connections_public_connection_edit_reconnect_failed', '重新连接失败'], - - ['connections_administration_deactivate_message', '您的连接设置将丢失。您要继续吗?'], -]; diff --git a/webapp/packages/plugin-connections-administration/src/manifest.ts b/webapp/packages/plugin-connections-administration/src/manifest.ts index a1564cd91a..29b491cd2a 100644 --- a/webapp/packages/plugin-connections-administration/src/manifest.ts +++ b/webapp/packages/plugin-connections-administration/src/manifest.ts @@ -1,32 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { ConnectionsAdministrationNavService } from './Administration/Connections/ConnectionsAdministrationNavService'; -import { ConnectionsAdministrationService } from './Administration/Connections/ConnectionsAdministrationService'; -import { CreateConnectionBaseBootstrap } from './Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap'; -import { ConnectionManualService } from './Administration/Connections/CreateConnection/Manual/ConnectionManualService'; -import { CreateConnectionService } from './Administration/Connections/CreateConnectionService'; -import { ConnectionAccessTabService } from './ConnectionForm/ConnectionAccess/ConnectionAccessTabService'; -import { LocaleService } from './LocaleService'; - export const connectionPlugin: PluginManifest = { info: { name: 'Connections Administration plugin', }, - - providers: [ - LocaleService, - ConnectionsAdministrationService, - ConnectionsAdministrationNavService, - CreateConnectionService, - ConnectionManualService, - CreateConnectionBaseBootstrap, - ConnectionAccessTabService, - ], }; diff --git a/webapp/packages/plugin-connections-administration/src/module.ts b/webapp/packages/plugin-connections-administration/src/module.ts new file mode 100644 index 0000000000..ed26bb3851 --- /dev/null +++ b/webapp/packages/plugin-connections-administration/src/module.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, ModuleRegistry } from '@cloudbeaver/core-di'; +import { ConnectionFormAccessTabBootstrap } from './ConnectionFormAccess/ConnectionFormAccessTabBootstrap.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-connections-administration', + + configure: serviceCollection => { + serviceCollection.addSingleton(Bootstrap, ConnectionFormAccessTabBootstrap); + }, +}); diff --git a/webapp/packages/plugin-connections-administration/tsconfig.json b/webapp/packages/plugin-connections-administration/tsconfig.json index 8c61a5a177..8bb1051b53 100644 --- a/webapp/packages/plugin-connections-administration/tsconfig.json +++ b/webapp/packages/plugin-connections-administration/tsconfig.json @@ -1,58 +1,62 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-connections/tsconfig.json" + "path": "../../common-typescript/@dbeaver/js-helpers" }, { - "path": "../core-administration/tsconfig.json" + "path": "../core-administration" }, { - "path": "../core-authentication/tsconfig.json" + "path": "../core-authentication" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-connections" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-executor/tsconfig.json" + "path": "../core-executor" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-projects/tsconfig.json" + "path": "../core-projects" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-sdk" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-utils" + }, + { + "path": "../plugin-connections" } ], "include": [ @@ -64,7 +68,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-connections/package.json b/webapp/packages/plugin-connections/package.json index d2384bc086..6b684cc1a7 100644 --- a/webapp/packages/plugin-connections/package.json +++ b/webapp/packages/plugin-connections/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-connections", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,44 +11,52 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-authentication": "~0.1.0", - "@cloudbeaver/plugin-navigation-tree": "~0.1.0", - "@cloudbeaver/plugin-projects": "~0.1.0", - "@cloudbeaver/plugin-top-app-bar": "~0.1.0", - "@cloudbeaver/core-authentication": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-data-context": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-projects": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-authentication": "workspace:*", + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-links": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-navigation-tree": "workspace:*", + "@cloudbeaver/core-projects": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-settings": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-authentication": "workspace:*", + "@cloudbeaver/plugin-navigation-tree": "workspace:*", + "@cloudbeaver/plugin-projects": "workspace:*", + "@cloudbeaver/plugin-top-app-bar": "workspace:*", + "@dbeaver/jdbc-uri-parser": "workspace:^", + "@dbeaver/js-helpers": "workspace:^", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-connections/public/icons/folder.svg b/webapp/packages/plugin-connections/public/icons/folder.svg index 2d5ef658a2..876915fbbc 100644 --- a/webapp/packages/plugin-connections/public/icons/folder.svg +++ b/webapp/packages/plugin-connections/public/icons/folder.svg @@ -1,7 +1,13 @@ - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-connections/public/icons/folder_m.svg b/webapp/packages/plugin-connections/public/icons/folder_m.svg index 864b689f54..e54a3499b7 100644 --- a/webapp/packages/plugin-connections/public/icons/folder_m.svg +++ b/webapp/packages/plugin-connections/public/icons/folder_m.svg @@ -1,7 +1,13 @@ - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-connections/public/icons/folder_sm.svg b/webapp/packages/plugin-connections/public/icons/folder_sm.svg index cfe6fc4d7b..a7ddd085bf 100644 --- a/webapp/packages/plugin-connections/public/icons/folder_sm.svg +++ b/webapp/packages/plugin-connections/public/icons/folder_sm.svg @@ -1,7 +1,13 @@ - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-connections/public/icons/plugin_connection_new.svg b/webapp/packages/plugin-connections/public/icons/plugin_connection_new.svg new file mode 100644 index 0000000000..d6df40808b --- /dev/null +++ b/webapp/packages/plugin-connections/public/icons/plugin_connection_new.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-connections/public/icons/plugin_connection_new_m.svg b/webapp/packages/plugin-connections/public/icons/plugin_connection_new_m.svg new file mode 100644 index 0000000000..dfe17716d6 --- /dev/null +++ b/webapp/packages/plugin-connections/public/icons/plugin_connection_new_m.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-connections/public/icons/plugin_connection_new_sm.svg b/webapp/packages/plugin-connections/public/icons/plugin_connection_new_sm.svg new file mode 100644 index 0000000000..0d3120ed5d --- /dev/null +++ b/webapp/packages/plugin-connections/public/icons/plugin_connection_new_sm.svg @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_CONNECTION.ts b/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_CONNECTION.ts new file mode 100644 index 0000000000..a0bea59cca --- /dev/null +++ b/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_CONNECTION.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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_TREE_CREATE_CONNECTION = createAction('create-tree-connection', { + label: 'plugin_connections_connection_create_menu_title', + tooltip: 'plugin_connections_connection_create_menu_title', +}); diff --git a/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_FOLDER.ts b/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_FOLDER.ts new file mode 100644 index 0000000000..bd57b1ef06 --- /dev/null +++ b/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_FOLDER.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_TREE_CREATE_FOLDER = createAction('create-tree-folder', { + label: 'ui_folder_new', +}); diff --git a/webapp/packages/plugin-connections/src/Actions/MENU_TREE_CREATE_CONNECTION.ts b/webapp/packages/plugin-connections/src/Actions/MENU_TREE_CREATE_CONNECTION.ts new file mode 100644 index 0000000000..91907fff26 --- /dev/null +++ b/webapp/packages/plugin-connections/src/Actions/MENU_TREE_CREATE_CONNECTION.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_TREE_CREATE_CONNECTION = createMenu('create-tree-connection', { + label: 'plugin_connections_connection_create_menu_title', + icon: '/icons/plugin_connection_new_sm.svg', + tooltip: 'plugin_connections_connection_create_menu_title', + group: true, +}); diff --git a/webapp/packages/plugin-connections/src/CONNECTIONS_SETTINGS_GROUP.ts b/webapp/packages/plugin-connections/src/CONNECTIONS_SETTINGS_GROUP.ts deleted file mode 100644 index 63f04450be..0000000000 --- a/webapp/packages/plugin-connections/src/CONNECTIONS_SETTINGS_GROUP.ts +++ /dev/null @@ -1,25 +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 { createSettingsGroup, type SettingsData } from '@cloudbeaver/core-settings'; - -export const CONNECTIONS_SETTINGS_GROUP = createSettingsGroup('settings_connections'); - -export const settings: SettingsData = { - scopeType: 'plugin', - scope: 'connections', - settingsData: [ - // TODO: it's administrator settings - // { - // key: 'hideConnectionViewForUsers', - // type: FormFieldType.Checkbox, - // name: 'settings_connections_hide_connections_view_name', - // description: 'settings_connections_hide_connections_view_description', - // groupId: CONNECTIONS_SETTINGS_GROUP.id, - // }, - ], -}; diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthService.ts b/webapp/packages/plugin-connections/src/ConnectionAuthService.ts index 117b47e6ba..c0f6afd9b2 100644 --- a/webapp/packages/plugin-connections/src/ConnectionAuthService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionAuthService.ts @@ -1,49 +1,74 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { AuthProviderService } from '@cloudbeaver/core-authentication'; +import { AuthProviderService, UserInfoResource } from '@cloudbeaver/core-authentication'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { - Connection, + type Connection, + ConnectionInfoAuthPropertiesResource, + type ConnectionInfoNetworkHandlers, + ConnectionInfoNetworkHandlersResource, ConnectionInfoResource, ConnectionsManagerService, createConnectionParam, - IConnectionInfoParams, + type IConnectionInfoParams, + type IRequireConnectionExecutorData, } from '@cloudbeaver/core-connections'; -import { Dependency, injectable } from '@cloudbeaver/core-di'; +import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { AuthenticationService } from '@cloudbeaver/plugin-authentication'; -import { DatabaseAuthDialog } from './DatabaseAuthDialog/DatabaseAuthDialog'; +const DatabaseAuthDialog = importLazyComponent(() => import('./DatabaseAuthDialog/DatabaseAuthDialog.js').then(m => m.DatabaseAuthDialog)); -@injectable() -export class ConnectionAuthService extends Dependency { +@injectable(() => [ + ConnectionInfoResource, + ConnectionInfoNetworkHandlersResource, + ConnectionInfoAuthPropertiesResource, + CommonDialogService, + AuthProviderService, + UserInfoResource, + ConnectionsManagerService, + AuthenticationService, +]) +export class ConnectionAuthService { constructor( private readonly connectionInfoResource: ConnectionInfoResource, + private readonly connectionInfoNetworkHandlersResource: ConnectionInfoNetworkHandlersResource, + private readonly connectionInfoAuthPropertiesResource: ConnectionInfoAuthPropertiesResource, private readonly commonDialogService: CommonDialogService, private readonly authProviderService: AuthProviderService, + userInfoResource: UserInfoResource, private readonly connectionsManagerService: ConnectionsManagerService, - private readonly notificationService: NotificationService, private readonly authenticationService: AuthenticationService, ) { - super(); - connectionsManagerService.connectionExecutor.addHandler(this.connectionDialog.bind(this)); - this.authenticationService.onLogout.before(connectionsManagerService.onDisconnect, state => ({ - connections: connectionInfoResource.values.filter(connection => connection.connected).map(createConnectionParam), - state, - })); + this.authenticationService.onLogin.before( + connectionsManagerService.onDisconnect, + state => ({ + connections: connectionInfoResource.values.filter(connection => connection.connected).map(createConnectionParam), + state, + }), + state => state === 'before' && userInfoResource.isAnonymous(), + ); + this.authenticationService.onLogout.before( + connectionsManagerService.onDisconnect, + state => ({ + connections: connectionInfoResource.values.filter(connection => connection.connected).map(createConnectionParam), + state, + }), + state => state === 'before', + ); } - private async connectionDialog(connectionKey: IConnectionInfoParams, context: IExecutionContextProvider) { + private async connectionDialog(data: IRequireConnectionExecutorData, context: IExecutionContextProvider) { const connection = context.getContext(this.connectionsManagerService.connectionContext); - const newConnection = await this.auth(connectionKey); + const newConnection = await this.auth(data.key, data.resetCredentials); if (!newConnection?.connected) { return; @@ -51,17 +76,16 @@ export class ConnectionAuthService extends Dependency { connection.connection = newConnection; } - async auth(key: IConnectionInfoParams, resetCredentials?: boolean): Promise { + private async auth(key: IConnectionInfoParams, resetCredentials?: boolean): Promise { if (!this.connectionInfoResource.has(key)) { return null; } - let connection = this.connectionInfoResource.get(key); + let connectionNetworkHandlers: ConnectionInfoNetworkHandlers | null = null; + let connection = await this.connectionInfoResource.load(key); const isConnectedInitially = connection?.connected; - if (!connection?.connected) { - connection = await this.connectionInfoResource.refresh(key); - } else { + if (connection?.connected) { if (resetCredentials) { this.connectionInfoResource.close(key); } else { @@ -69,21 +93,26 @@ export class ConnectionAuthService extends Dependency { } } - if (connection.requiredAuth) { - const state = await this.authProviderService.requireProvider(connection.requiredAuth); + const connectionAuthProperties = await this.connectionInfoAuthPropertiesResource.load(key); + + if (connectionAuthProperties.requiredAuth) { + const state = await this.authProviderService.requireProvider(connectionAuthProperties.requiredAuth); if (!state) { return connection; } } - connection = await this.connectionInfoResource.load(key, ['includeAuthNeeded', 'includeNetworkHandlersConfig', 'includeCredentialsSaved']); + [connectionNetworkHandlers, connection] = await Promise.all([ + this.connectionInfoNetworkHandlersResource.load(key), + this.connectionInfoResource.load(key), + ]); - const networkHandlers = connection + const networkHandlers = connectionNetworkHandlers .networkHandlersConfig!.filter(handler => handler.enabled && (!handler.savePassword || resetCredentials)) .map(handler => handler.id); - if (connection.authNeeded || (connection.credentialsSaved && resetCredentials) || networkHandlers.length > 0) { + if (connectionAuthProperties.authNeeded || (connectionAuthProperties.credentialsSaved && resetCredentials) || networkHandlers.length > 0) { const result = await this.commonDialogService.open(DatabaseAuthDialog, { connection: key, networkHandlers, diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialog.m.css b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialog.m.css deleted file mode 100644 index 6472b4a0a5..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialog.m.css +++ /dev/null @@ -1,13 +0,0 @@ -.submittingForm { - overflow: auto; - margin: auto; - flex: 1; - display: flex; - flex-direction: column; -} -.connectionAuthenticationFormLoader { - align-content: center; -} -.button { - margin-left: auto; -} diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialog.module.css b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialog.module.css new file mode 100644 index 0000000000..fadb46eeef --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialog.module.css @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.submittingForm { + overflow: auto; + margin: auto; + flex: 1; + display: flex; + flex-direction: column; +} +.connectionAuthenticationFormLoader { + align-content: center; +} +.button { + margin-left: auto; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialog.tsx b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialog.tsx index a0cd46544c..0becfde696 100644 --- a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialog.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialog.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -23,14 +23,14 @@ import { useDBDriver } from '@cloudbeaver/core-connections'; import type { DialogComponent } from '@cloudbeaver/core-dialogs'; import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; -import style from './ConnectionAuthenticationDialog.m.css'; -import { ConnectionAuthenticationFormLoader } from './ConnectionAuthenticationFormLoader'; +import style from './ConnectionAuthenticationDialog.module.css'; +import { ConnectionAuthenticationFormLoader } from './ConnectionAuthenticationFormLoader.js'; export interface ConnectionAuthenticationDialogPayload { config: ConnectionConfig; authModelId: string | null; networkHandlers?: string[]; - driverId?: string; + projectId: string | null; } export const ConnectionAuthenticationDialog: DialogComponent = observer( @@ -38,7 +38,7 @@ export const ConnectionAuthenticationDialog: DialogComponent({ focusFirstChild: true }); - const { driver } = useDBDriver(payload.driverId || ''); + const { driver } = useDBDriver(payload.config.driverId || ''); return ( @@ -52,16 +52,17 @@ export const ConnectionAuthenticationDialog: DialogComponent resolveDialog()}> - diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialogLoader.ts b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialogLoader.ts new file mode 100644 index 0000000000..f1f87ec502 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialogLoader.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const ConnectionAuthenticationDialogLoader = importLazyComponent(() => + import('./ConnectionAuthenticationDialog.js').then(module => module.ConnectionAuthenticationDialog), +); diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialogLoader.tsx b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialogLoader.tsx deleted file mode 100644 index d84191c6d6..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationDialogLoader.tsx +++ /dev/null @@ -1,13 +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 React from 'react'; - -export const ConnectionAuthenticationDialogLoader = React.lazy(async () => { - const { ConnectionAuthenticationDialog } = await import('./ConnectionAuthenticationDialog'); - return { default: ConnectionAuthenticationDialog }; -}); diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationForm.tsx b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationForm.tsx index 058f7b3049..395ec6644d 100644 --- a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationForm.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationForm.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -18,10 +18,13 @@ import { useTranslate, } from '@cloudbeaver/core-blocks'; import { DatabaseAuthModelsResource } from '@cloudbeaver/core-connections'; +import { useService } from '@cloudbeaver/core-di'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; -import type { IConnectionAuthenticationConfig } from './IConnectionAuthenticationConfig'; -import { NetworkHandlers } from './NetworkHandlers'; +import type { IConnectionAuthenticationConfig } from './IConnectionAuthenticationConfig.js'; +import { NetworkHandlers } from './NetworkHandlers.js'; export interface ConnectionAuthenticationFormProps { config: Partial; @@ -33,6 +36,7 @@ export interface ConnectionAuthenticationFormProps { disabled?: boolean; className?: string; hideFeatures?: string[]; + projectId: string | null; } export const ConnectionAuthenticationForm = observer(function ConnectionAuthenticationForm({ @@ -41,6 +45,7 @@ export const ConnectionAuthenticationForm = observer @@ -90,6 +108,7 @@ export const ConnectionAuthenticationForm = observer + import('./ConnectionAuthenticationForm.js').then(m => m.ConnectionAuthenticationForm), +); diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationFormLoader.tsx b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationFormLoader.tsx deleted file mode 100644 index f2eb496816..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationFormLoader.tsx +++ /dev/null @@ -1,13 +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 React from 'react'; - -export const ConnectionAuthenticationFormLoader = React.lazy(async () => { - const { ConnectionAuthenticationForm } = await import('./ConnectionAuthenticationForm'); - return { default: ConnectionAuthenticationForm }; -}); diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/IConnectionAuthenticationConfig.ts b/webapp/packages/plugin-connections/src/ConnectionAuthentication/IConnectionAuthenticationConfig.ts index f38bb9f1ac..4816f85da8 100644 --- a/webapp/packages/plugin-connections/src/ConnectionAuthentication/IConnectionAuthenticationConfig.ts +++ b/webapp/packages/plugin-connections/src/ConnectionAuthentication/IConnectionAuthenticationConfig.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx b/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx index 430c273db5..a6f4298a67 100644 --- a/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,21 +9,35 @@ import { observer } from 'mobx-react-lite'; import { FieldCheckbox, GroupTitle, InputField, ObjectPropertyInfoForm, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; -import { NetworkHandlerAuthType, NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; +import { useService } from '@cloudbeaver/core-di'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { NetworkHandlerAuthType, type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; -import { SSHKeyUploader } from '../ConnectionForm/SSH/SSHKeyUploader'; -import { PROPERTY_FEATURE_SECURED } from '../ConnectionForm/SSL/PROPERTY_FEATURE_SECURED'; +import { SSHKeyUploader } from '../ConnectionForm/SSH/SSHKeyUploader.js'; +import { PROPERTY_FEATURE_SECURED } from '../ConnectionForm/SSL/PROPERTY_FEATURE_SECURED.js'; interface Props { id: string; networkHandlersConfig: NetworkHandlerConfigInput[]; allowSaveCredentials?: boolean; disabled?: boolean; + projectId: string | null; } -export const NetworkHandlerAuthForm = observer(function NetworkHandlerAuthForm({ id, networkHandlersConfig, allowSaveCredentials, disabled }) { +export const NetworkHandlerAuthForm = observer(function NetworkHandlerAuthForm({ + id, + networkHandlersConfig, + allowSaveCredentials, + disabled, + projectId, +}) { const translate = useTranslate(); const handler = useResource(NetworkHandlerAuthForm, NetworkHandlerResource, id); + const serverConfigResource = useResource(NetworkHandlerAuthForm, ServerConfigResource, undefined); + const distributed = Boolean(serverConfigResource?.data?.distributed); + const projectInfoResource = useService(ProjectInfoResource); + const isSharedProject = projectInfoResource.isProjectShared(projectId); //@TODO Do not mutate state in component body if (!networkHandlersConfig.some(state => state.id === id)) { @@ -53,10 +67,10 @@ export const NetworkHandlerAuthForm = observer(function NetworkHandlerAut {ssh && ( <> - + {translate(`connections_network_handler_${id}_user`, 'connections_network_handler_default_user')} - + {passwordLabel} @@ -67,10 +81,19 @@ export const NetworkHandlerAuthForm = observer(function NetworkHandlerAut )} {allowSaveCredentials && ( )} diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlers.tsx b/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlers.tsx index 672a691eab..499965f334 100644 --- a/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlers.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlers.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,16 +10,23 @@ import { observer } from 'mobx-react-lite'; import { Group } from '@cloudbeaver/core-blocks'; import type { NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; -import { NetworkHandlerAuthForm } from './NetworkHandlerAuthForm'; +import { NetworkHandlerAuthForm } from './NetworkHandlerAuthForm.js'; interface Props { networkHandlers: string[]; networkHandlersConfig: NetworkHandlerConfigInput[]; disabled?: boolean; allowSaveCredentials?: boolean; + projectId: string | null; } -export const NetworkHandlers = observer(function NetworkHandlers({ networkHandlers, networkHandlersConfig, allowSaveCredentials, disabled }) { +export const NetworkHandlers = observer(function NetworkHandlers({ + networkHandlers, + networkHandlersConfig, + allowSaveCredentials, + disabled, + projectId, +}) { if (!networkHandlers.length) { return null; } @@ -29,6 +36,7 @@ export const NetworkHandlers = observer(function NetworkHandlers({ networ {networkHandlers.map(handler => ( Promise; + test: () => Promise; + onCancel?: () => void; +} + +export const ConnectionFormActionsContext = createContext(null); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm.tsx new file mode 100644 index 0000000000..8eb0ca54b1 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm.tsx @@ -0,0 +1,56 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Container, ObjectPropertyInfoForm, type ILayoutSizeProps } from '@cloudbeaver/core-blocks'; +import { getObjectPropertyType, type ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import { isSafari } from '@cloudbeaver/core-utils'; + +interface Props { + credentials?: Record; + defaultCredentials?: Record; + properties: ReadonlyArray; + readonly?: boolean; + disabled?: boolean; +} + +export const ConnectionAuthModelCredentialsForm = observer(function ConnectionAuthModelCredentialsForm({ + credentials, + defaultCredentials, + properties, + readonly, + disabled, +}) { + function getLayoutSize(property: ObjectPropertyInfo): ILayoutSizeProps { + const type = getObjectPropertyType(property); + + if (type === 'checkbox') { + return {}; + } + + return { + tiny: true, + }; + } + + return ( + + + + ); +}); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthModelSelector.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthModelSelector.tsx new file mode 100644 index 0000000000..9d4e7c768f --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthModelSelector.tsx @@ -0,0 +1,61 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Combobox, usePermission, useResource } from '@cloudbeaver/core-blocks'; +import { DatabaseAuthModelsResource } from '@cloudbeaver/core-connections'; +import { CachedResourceListEmptyKey, resourceKeyList } from '@cloudbeaver/core-resource'; +import { EAdminPermission } from '@cloudbeaver/core-root'; +import { isNotNullDefined } from '@dbeaver/js-helpers'; + +interface Props { + authModelCredentialsState: { authModelId?: string }; + applicableAuthModels: string[]; + readonlyAuthModelId?: boolean; + readonly?: boolean; + disabled?: boolean; + onAuthModelChange?: (authModelId: string | undefined) => void; +} + +export const ConnectionAuthModelSelector = observer(function ConnectionAuthModelSelector({ + authModelCredentialsState, + applicableAuthModels, + onAuthModelChange, + readonlyAuthModelId, + readonly, + disabled, +}) { + const adminPermission = usePermission(EAdminPermission.admin); + + const authModelsLoader = useResource( + ConnectionAuthModelSelector, + DatabaseAuthModelsResource, + applicableAuthModels.length ? resourceKeyList(applicableAuthModels) : CachedResourceListEmptyKey, + ); + + const availableAuthModels = authModelsLoader.data.filter(isNotNullDefined).filter(model => adminPermission || !model.requiresLocalConfiguration); + + if (availableAuthModels.length <= 1) { + return null; + } + + return ( + model.id} + valueSelector={model => model.displayName} + titleSelector={model => model.description} + readOnly={readonly || readonlyAuthModelId} + disabled={disabled} + tiny + fill + onSelect={onAuthModelChange} + /> + ); +}); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionForm.module.css b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionForm.module.css new file mode 100644 index 0000000000..617753cb3f --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionForm.module.css @@ -0,0 +1,75 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tabList { + position: relative; + flex-shrink: 0; + align-items: center; +} + +.connectionTopBar { + composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global; +} + +.connectionTopBar { + position: relative; + display: flex; + padding-top: 16px; + + &:before { + content: ''; + position: absolute; + bottom: 0; + width: 100%; + border-bottom: solid 2px; + border-color: inherit; + } +} + +.connectionTopBarTabs { + flex: 1; +} + +.connectionTopBarActions { + display: flex; + align-items: center; + padding: 0 24px; + gap: 16px; +} + +.connectionStatusMessage { + composes: theme-typography--caption from global; + height: 24px; + padding: 0 16px; + display: flex; + align-items: center; + gap: 8px; + + & .iconOrImage { + height: 24px; + width: 24px; + } +} + +.box { + composes: theme-background-secondary theme-text-on-secondary from global; + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + overflow: auto; +} + +.contentBox { + composes: theme-background-secondary theme-border-color-background from global; + position: relative; + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionForm.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionForm.tsx index 2b14c1a281..7f08b24b57 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionForm.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionForm.tsx @@ -1,163 +1,110 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useEffect } from 'react'; -import styled, { css } from 'reshadow'; -import { ExceptionMessage, Loader, Placeholder, StatusMessage, useExecutor, useObjectRef, useStyles } from '@cloudbeaver/core-blocks'; +import { Container, Form, Loader, Placeholder, s, StatusMessage, useForm, useObjectRef, useS } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { ENotificationType } from '@cloudbeaver/core-events'; +import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; -import { BASE_TAB_STYLES, TabList, TabPanelList, TabsState, UNDERLINE_TAB_BIG_STYLES, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui'; +import { formSubmitContext, TabList, TabPanelList, TabsState, type IFormState } from '@cloudbeaver/core-ui'; -import { ConnectionFormService } from './ConnectionFormService'; -import { connectionConfigContext } from './Contexts/connectionConfigContext'; -import type { IConnectionFormState } from './IConnectionFormProps'; - -const tabsStyles = css` - TabList { - position: relative; - flex-shrink: 0; - align-items: center; - } -`; - -const topBarStyles = css` - connection-top-bar { - composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global; - } - connection-top-bar { - position: relative; - display: flex; - padding-top: 16px; - - &:before { - content: ''; - position: absolute; - bottom: 0; - width: 100%; - border-bottom: solid 2px; - border-color: inherit; - } - } - connection-top-bar-tabs { - flex: 1; - } - - connection-top-bar-actions { - display: flex; - align-items: center; - padding: 0 24px; - gap: 16px; - } - - /*Button:not(:first-child) { - margin-right: 24px; - }*/ - - connection-status-message { - composes: theme-typography--caption from global; - height: 24px; - padding: 0 16px; - display: flex; - align-items: center; - gap: 8px; - - & IconOrImage { - height: 24px; - width: 24px; - } - } -`; - -const formStyles = css` - box { - composes: theme-background-secondary theme-text-on-secondary from global; - display: flex; - flex-direction: column; - flex: 1; - height: 100%; - overflow: auto; - } - content-box { - composes: theme-background-secondary theme-border-color-background from global; - position: relative; - display: flex; - flex: 1; - flex-direction: column; - overflow: auto; - } -`; +import { ConnectionFormActionsContext, type IConnectionFormActionsContext } from './ConnectFormActionsContext.js'; +import style from './ConnectionForm.module.css'; +import type { ConnectionFormState } from './ConnectionFormState.js'; +import { getFirstException } from '@cloudbeaver/core-utils'; +import { ConnectionFormService } from './ConnectionFormService.js'; +import { getConnectionFormOptionsPart } from './Options/getConnectionFormOptionsPart.js'; +import { ExecutionContext } from '@cloudbeaver/core-executor'; +import type { IConnectionFormState } from './IConnectionFormState.js'; export interface ConnectionFormProps { - state: IConnectionFormState; + formState: ConnectionFormState; onCancel?: () => void; onSave?: (config: ConnectionConfig) => void; className?: string; } -export const ConnectionForm = observer(function ConnectionForm({ state, onCancel, onSave = () => {}, className }) { - const props = useObjectRef({ onSave }); - const style = [BASE_TAB_STYLES, tabsStyles, UNDERLINE_TAB_STYLES, UNDERLINE_TAB_BIG_STYLES]; - const styles = useStyles(style, topBarStyles, formStyles); +export const ConnectionForm = observer(function ConnectionForm({ formState, onCancel, onSave = () => {}, className }) { const service = useService(ConnectionFormService); + const styles = useS(style); + const notificationService = useService(NotificationService); + const optionsPart = getConnectionFormOptionsPart(formState); + const exception = getFirstException(formState.exception); + + const form = useForm({ + onSubmit: async event => { + const submitType = event?.type === 'test' ? 'test' : 'submit'; + const context = new ExecutionContext>(formState); + + const submitInfo = context.getContext(formSubmitContext); + submitInfo.setType(submitType); + if (submitType === 'test') { + submitInfo.setSubmitOnNoChanges(true); + } + const initialMode = formState.mode; + const saved = await formState.save(context); + + if (saved) { + if (submitType === 'submit') { + notificationService.notify( + { + title: initialMode === 'create' ? 'core_connections_connection_create_success' : 'core_connections_connection_update_success', + message: optionsPart.state?.name, + }, + ENotificationType.Success, + ); + + onSave(optionsPart.state); + } + } else { + const error = getFirstException(formState.exception); - useExecutor({ - executor: state.submittingTask, - postHandlers: [ - function save(data, contexts) { - const validation = contexts.getContext(service.connectionValidationContext); - const state = contexts.getContext(service.connectionStatusContext); - const config = contexts.getContext(connectionConfigContext); - - if (validation.valid && state.saved && data.submitType === 'submit') { - props.onSave(config); + if (submitType === 'submit') { + notificationService.logException(error, 'connections_connection_create_fail'); } - }, - ], + } + }, }); - useEffect(() => { - state.loadConnectionInfo(); - }, [state]); - - if (state.initError) { - return styled(styles)( state.loadConnectionInfo()} />); - } - - if (!state.configured) { - return styled(styles)( - - - , - ); - } - - return styled(styles)( - - - - - - - - - - - - - - - - - - - - , + const actionsContext = useObjectRef(() => ({ + save: () => form.submit(new SubmitEvent('submit')), + test: () => form.submit(new SubmitEvent('test')), + onCancel, + })); + + return ( +
+ +
+
+ + + + + + +
+ + + + + +
+
+
+ +
+
+
+
); }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx index c0c0c47a79..6b9b78ea8e 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx @@ -1,50 +1,62 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; -import { Button, getComputed, PlaceholderComponent, useResource, useTranslate } from '@cloudbeaver/core-blocks'; -import { DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections'; -import { useAuthenticationAction } from '@cloudbeaver/core-ui'; +import { Button, getComputed, type PlaceholderComponent, useResource, useTranslate, useAuthenticationAction } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoAuthPropertiesResource, DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; -import type { IConnectionFormProps } from './IConnectionFormProps'; +import { ConnectionFormActionsContext } from './ConnectFormActionsContext.js'; +import type { IConnectionFormProps } from './IConnectionFormState.js'; +import { getConnectionFormOptionsPart } from './Options/getConnectionFormOptionsPart.js'; +export const ConnectionFormBaseActions: PlaceholderComponent = observer(function ConnectionFormBaseActions({ formState }) { + const actions = useContext(ConnectionFormActionsContext); -export const ConnectionFormBaseActions: PlaceholderComponent = observer(function ConnectionFormBaseActions({ - state, - onCancel, -}) { - const translate = useTranslate(); - const driverMap = useResource(ConnectionFormBaseActions, DBDriverResource, state.config.driverId || null); + if (!actions) { + throw new Error('ConnectionFormActionsContext not provided'); + } + const translate = useTranslate(); + const optionsPart = getConnectionFormOptionsPart(formState); + const driverMap = useResource(ConnectionFormBaseActions, DBDriverResource, optionsPart.state.driverId || null); const driver = driverMap.data; + const connectionInfoResource = useResource(ConnectionFormBaseActions, ConnectionInfoAuthPropertiesResource, optionsPart.connectionKey); + + const serverConfigResource = useResource(ConnectionFormBaseActions, ServerConfigResource, undefined); const { data: authModel } = useResource( ConnectionFormBaseActions, DatabaseAuthModelsResource, - getComputed(() => state.config.authModelId || state.info?.authModel || driver?.defaultAuthModel || null), + getComputed(() => optionsPart.state.authModelId || connectionInfoResource.data?.authModel || driver?.defaultAuthModel || null), ); const authentication = useAuthenticationAction({ - providerId: authModel?.requiredAuth ?? state.info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID, + providerId: authModel?.requiredAuth ?? connectionInfoResource.data?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID, }); const authorized = authentication.providerId === AUTH_PROVIDER_LOCAL_ID || authentication.authorized; + const disableTest = serverConfigResource.data?.distributed && !!optionsPart.state.sharedCredentials; return ( <> - {onCancel && ( - )} - - + )} + ); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActionsLoader.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActionsLoader.tsx index 4904f1e01c..4a98ed9c58 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActionsLoader.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActionsLoader.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,6 +8,6 @@ import React from 'react'; export const ConnectionFormBaseActionsLoader = React.lazy(async () => { - const { ConnectionFormBaseActions } = await import('./ConnectionFormBaseActions'); + const { ConnectionFormBaseActions } = await import('./ConnectionFormBaseActions.js'); return { default: ConnectionFormBaseActions }; }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormLoader.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormLoader.tsx index 77b70de265..6f7380bb64 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormLoader.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormLoader.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,6 +8,6 @@ import React from 'react'; export const ConnectionFormLoader = React.lazy(async () => { - const { ConnectionForm } = await import('./ConnectionForm'); + const { ConnectionForm } = await import('./ConnectionForm.js'); return { default: ConnectionForm }; }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormService.ts index 076313f2bd..38ddad94ef 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormService.ts @@ -1,182 +1,30 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { observable, runInAction, toJS } from 'mobx'; - -import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; -import { ExecutorHandlersCollection, ExecutorInterrupter, IExecutorHandler, IExecutorHandlersCollection } from '@cloudbeaver/core-executor'; +import { NotificationService } from '@cloudbeaver/core-events'; import { LocalizationService } from '@cloudbeaver/core-localization'; -import { TabsContainer } from '@cloudbeaver/core-ui'; - -import { ConnectionAuthenticationDialogLoader } from '../ConnectionAuthentication/ConnectionAuthenticationDialogLoader'; -import { ConnectionFormBaseActionsLoader } from './ConnectionFormBaseActionsLoader'; -import { connectionConfigContext } from './Contexts/connectionConfigContext'; -import { connectionCredentialsStateContext } from './Contexts/connectionCredentialsStateContext'; -import type { IConnectionFormFillConfigData, IConnectionFormProps, IConnectionFormState, IConnectionFormSubmitData } from './IConnectionFormProps'; - -export interface IConnectionFormValidation { - valid: boolean; - messages: string[]; - info: (message: string) => void; - error: (message: string) => void; -} - -export interface IConnectionFormStatus { - saved: boolean; - messages: string[]; - exception: Error | null; - info: (message: string) => void; - error: (message: string, exception?: Error) => void; -} - -@injectable() -export class ConnectionFormService { - readonly tabsContainer: TabsContainer; - readonly actionsContainer: PlaceholderContainer; +import { FormBaseService, type IFormState } from '@cloudbeaver/core-ui'; +import type { IConnectionFormProps, IConnectionFormState } from './IConnectionFormState.js'; +import { importLazyComponent, PlaceholderContainer } from '@cloudbeaver/core-blocks'; - readonly configureTask: IExecutorHandlersCollection; - readonly fillConfigTask: IExecutorHandlersCollection; - readonly prepareConfigTask: IExecutorHandlersCollection; - readonly formValidationTask: IExecutorHandlersCollection; - readonly formSubmittingTask: IExecutorHandlersCollection; - readonly formStateTask: IExecutorHandlersCollection; +const ConnectionFormBaseActionsLoader = importLazyComponent(() => import('./ConnectionFormBaseActions.js').then(m => m.ConnectionFormBaseActions)); - constructor( - private readonly notificationService: NotificationService, - private readonly commonDialogService: CommonDialogService, - private readonly localizationService: LocalizationService, - ) { - this.tabsContainer = new TabsContainer('Connection settings'); - this.actionsContainer = new PlaceholderContainer(); - this.configureTask = new ExecutorHandlersCollection(); - this.fillConfigTask = new ExecutorHandlersCollection(); - this.prepareConfigTask = new ExecutorHandlersCollection(); - this.formSubmittingTask = new ExecutorHandlersCollection(); - this.formValidationTask = new ExecutorHandlersCollection(); - this.formStateTask = new ExecutorHandlersCollection(); +export type ProviderPropertiesContainerFormProps = { + formState: IFormState; +}; - this.formSubmittingTask.before(this.formValidationTask).before(this.prepareConfigTask); - - this.formStateTask.before(this.prepareConfigTask, state => ({ state, submitType: 'submit' })); - - this.prepareConfigTask.addPostHandler(this.askCredentials); - this.formSubmittingTask.addPostHandler(this.showSubmittingStatusMessage); - this.formValidationTask.addPostHandler(this.ensureValidation); +@injectable(() => [LocalizationService, NotificationService]) +export class ConnectionFormService extends FormBaseService { + readonly providerPropertiesContainer: PlaceholderContainer; + constructor(localizationService: LocalizationService, notificationService: NotificationService) { + super(localizationService, notificationService, 'Connection form'); + this.providerPropertiesContainer = new PlaceholderContainer(); this.actionsContainer.add(ConnectionFormBaseActionsLoader); } - - connectionValidationContext = (): IConnectionFormValidation => ({ - valid: true, - messages: [], - info(message: string) { - this.messages.push(message); - }, - error(message: string) { - this.messages.push(message); - this.valid = false; - }, - }); - - connectionStatusContext = (): IConnectionFormStatus => ({ - saved: true, - messages: [], - exception: null, - info(message: string) { - this.messages.push(message); - }, - error(message: string, exception: Error | null = null) { - this.messages.push(message); - this.saved = false; - this.exception = exception; - }, - }); - - private readonly showSubmittingStatusMessage: IExecutorHandler = (data, contexts) => { - const status = contexts.getContext(this.connectionStatusContext); - - if (!status.saved) { - ExecutorInterrupter.interrupt(contexts); - } - - if (status.messages.length > 0) { - if (status.exception) { - this.notificationService.logException(status.exception, status.messages[0], status.messages.slice(1).join('\n')); - } else { - this.notificationService.notify( - { - title: status.messages[0], - message: status.messages.slice(1).join('\n'), - }, - status.saved ? ENotificationType.Success : ENotificationType.Error, - ); - } - } - }; - - private readonly askCredentials: IExecutorHandler = async (data, contexts) => { - const credentialsState = contexts.getContext(connectionCredentialsStateContext); - - if (data.submitType !== 'test' || (!credentialsState.authModelId && !credentialsState.networkHandlers.length)) { - return; - } - - const config = contexts.getContext(connectionConfigContext); - - runInAction(() => { - if (credentialsState.authModelId) { - if (!config.credentials) { - config.credentials = { ...data.state.config.credentials }; - } - - config.credentials = observable(config.credentials); - } - - if (credentialsState.networkHandlers.length > 0) { - if (!config.networkHandlersConfig) { - config.networkHandlersConfig = toJS(data.state.config.networkHandlersConfig) || []; - } - - config.networkHandlersConfig = observable(config.networkHandlersConfig); - } - }); - - const result = await this.commonDialogService.open(ConnectionAuthenticationDialogLoader, { - config, - authModelId: credentialsState.authModelId, - networkHandlers: credentialsState.networkHandlers, - driverId: config.driverId, - }); - - if (result === DialogueStateResult.Rejected) { - ExecutorInterrupter.interrupt(contexts); - } - }; - - private readonly ensureValidation: IExecutorHandler = (data, contexts) => { - const validation = contexts.getContext(this.connectionValidationContext); - - if (!validation.valid) { - ExecutorInterrupter.interrupt(contexts); - } - - if (validation.messages.length > 0) { - const messages = validation.messages.map(message => this.localizationService.translate(message)); - this.notificationService.notify( - { - title: - data.state.mode === 'edit' ? 'connections_administration_connection_save_error' : 'connections_administration_connection_create_error', - message: messages.join('\n'), - }, - validation.valid ? ENotificationType.Info : ENotificationType.Error, - ); - } - }; } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts index aff0455fcd..13d06e8d7f 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts @@ -1,323 +1,18 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, computed, makeObservable, observable } from 'mobx'; +import type { IServiceProvider } from '@cloudbeaver/core-di'; +import { FormState } from '@cloudbeaver/core-ui'; -import { ConnectionInfoResource, createConnectionParam, DatabaseConnection, IConnectionInfoParams } from '@cloudbeaver/core-connections'; -import { Executor, IExecutionContextProvider, IExecutor } from '@cloudbeaver/core-executor'; -import type { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; -import type { ResourceKeySimple } from '@cloudbeaver/core-resource'; -import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; -import { formStateContext, type IFormStateInfo } from '@cloudbeaver/core-ui'; -import { MetadataMap, uuid } from '@cloudbeaver/core-utils'; +import { ConnectionFormService } from './ConnectionFormService.js'; +import type { IConnectionFormState } from './IConnectionFormState.js'; -import { connectionFormConfigureContext } from './connectionFormConfigureContext'; -import type { ConnectionFormService } from './ConnectionFormService'; -import type { ConnectionFormMode, ConnectionFormType, IConnectionFormState, IConnectionFormSubmitData } from './IConnectionFormProps'; - -export class ConnectionFormState implements IConnectionFormState { - mode: ConnectionFormMode; - type: ConnectionFormType; - projectId: string | null; - - config: ConnectionConfig; - - partsState: MetadataMap; - - statusMessage: string | string[] | null; - configured: boolean; - initError: Error | null; - - get loading(): boolean { - return this.loadConnectionTask.executing || this.submittingTask.executing; - } - - get disabled(): boolean { - return this.loading || !!this.stateInfo?.disabled || this.loadConnectionTask.executing; - } - - get availableDrivers(): string[] { - if (this._availableDrivers.length === 0 && this.config.driverId) { - return [this.config.driverId]; - } - - return this._availableDrivers; - } - - get info(): DatabaseConnection | undefined { - if (!this.config.connectionId || this.projectId === null) { - return undefined; - } - - return this.resource.get(createConnectionParam(this.projectId, this.config.connectionId)); - } - - get readonly(): boolean { - if (this.stateInfo?.readonly) { - return true; - } - - if (this.type === 'admin' || this.mode === 'create') { - return false; - } - - if (!this.info?.canEdit) { - return true; - } - - return false; - } - - get id(): string { - if (this.mode === 'create') { - return 'create'; - } - - return this.config.connectionId || this._id; - } - - readonly resource: ConnectionInfoResource; - readonly service: ConnectionFormService; - readonly submittingTask: IExecutor; - readonly closeTask: IExecutor; - - private readonly _id: string; - private stateInfo: IFormStateInfo | null; - private readonly loadConnectionTask: IExecutor; - private readonly formStateTask: IExecutor; - private _availableDrivers: string[]; - - constructor( - private readonly projectsService: ProjectsService, - private readonly projectInfoResource: ProjectInfoResource, - service: ConnectionFormService, - resource: ConnectionInfoResource, - ) { - this._id = uuid(); - this.initError = null; - - this.resource = resource; - this.projectId = null; - this.config = {}; - this._availableDrivers = []; - this.stateInfo = null; - this.partsState = new MetadataMap(); - this.service = service; - this.formStateTask = new Executor(this, () => true); - this.loadConnectionTask = new Executor(this, () => true); - this.submittingTask = new Executor(); - this.closeTask = new Executor(); - this.statusMessage = null; - this.configured = false; - this.mode = 'create'; - this.type = 'public'; - - this.syncProject = this.syncProject.bind(this); - this.syncInfo = this.syncInfo.bind(this); - this.test = this.test.bind(this); - this.save = this.save.bind(this); - this.checkFormState = this.checkFormState.bind(this); - this.loadInfo = this.loadInfo.bind(this); - this.updateFormState = this.updateFormState.bind(this); - - this.formStateTask.addCollection(service.formStateTask).addPostHandler(this.updateFormState); - - this.resource.onItemUpdate.addHandler(this.syncInfo); - - this.projectInfoResource.onDataUpdate.addHandler(this.syncProject); - - this.projectsService.onActiveProjectChange.addHandler(this.syncProject); - - this.submittingTask.addPostHandler(async (data, contexts) => { - const status = contexts.getContext(service.connectionStatusContext); - const validation = contexts.getContext(service.connectionValidationContext); - - if (data.submitType !== 'submit' || !status.saved || !validation.valid) { - return; - } - - this.reset(); - await this.load(); - }); - - this.loadConnectionTask - .before(service.configureTask) - .addPostHandler(this.loadInfo) - .next(service.fillConfigTask, (state, contexts) => { - const configuration = contexts.getContext(connectionFormConfigureContext); - - return { - state, - updated: state.info !== configuration.info || state.config.driverId !== configuration.driverId || !this.configured, - }; - }) - .next(this.formStateTask); - - makeObservable(this, { - projectId: observable, - mode: observable, - type: observable, - config: observable, - availableDrivers: computed, - _availableDrivers: observable, - info: computed, - statusMessage: observable, - configured: observable, - readonly: computed, - stateInfo: observable, - initError: observable.ref, - id: computed, - reset: action, - setOptions: action, - setConfig: action, - setAvailableDrivers: action, - updateFormState: action, - }); - } - - async loadConnectionInfo(): Promise { - try { - this.initError = null; - await this.loadConnectionTask.execute(this); - - return this.info; - } catch (exception: any) { - this.initError = exception; - throw exception; - } - } - - async load(): Promise { - await this.loadConnectionInfo(); - } - - reset(): void { - this.configured = false; - this.partsState.clear(); - } - - setPartsState(state: MetadataMap): this { - this.partsState = state; - return this; - } - - setOptions(mode: ConnectionFormMode, type: ConnectionFormType): this { - this.mode = mode; - this.type = type; - return this; - } - - setConfig(projectId: string, config: ConnectionConfig): this { - this.setProject(projectId); - this.config = config; - this.reset(); - return this; - } - - setProject(projectId: string): this { - this.projectId = projectId; - return this; - } - - setAvailableDrivers(drivers: string[]): this { - this._availableDrivers = drivers; - this.reset(); - return this; - } - - async save(): Promise { - await this.submittingTask.executeScope( - { - state: this, - submitType: 'submit', - }, - this.service.formSubmittingTask, - ); - } - - async test(): Promise { - await this.submittingTask.executeScope( - { - state: this, - submitType: 'test', - }, - this.service.formSubmittingTask, - ); - } - - async checkFormState(): Promise { - await this.loadConnectionInfo(); - return this.stateInfo; - } - - dispose(): void { - this.resource.onItemUpdate.removeHandler(this.syncInfo); - this.projectInfoResource.onDataUpdate.removeHandler(this.syncProject); - this.projectsService.onActiveProjectChange.removeHandler(this.syncProject); - } - - async close(): Promise { - await this.closeTask.execute(); - } - - private updateFormState(data: IConnectionFormState, contexts: IExecutionContextProvider): void { - const context = contexts.getContext(formStateContext); - - if (this.mode === 'create') { - context.markEdited(); - } - - this.statusMessage = context.statusMessage; - - if (this.statusMessage === null && this.mode === 'edit') { - if (!this.info?.canEdit) { - this.statusMessage = 'connections_connection_edit_not_own_deny'; - } - } - - this.stateInfo = context; - this.configured = true; - } - - private syncInfo(key: ResourceKeySimple) { - if ( - !this.config.connectionId || - this.projectId === null || - !this.resource.isIntersect(key, createConnectionParam(this.projectId, this.config.connectionId)) - ) { - return; - } - - this.loadConnectionInfo(); - } - - private async syncProject() { - if (!this.projectId) { - return; - } - - const project = this.projectInfoResource.get(this.projectId); - if (!project?.canEditDataSources || !this.projectsService.activeProjects.includes(project)) { - await this.close(); - } - } - - private async loadInfo(data: IConnectionFormState, contexts: IExecutionContextProvider) { - if (!data.config.connectionId || data.projectId === null) { - return; - } - - const key = createConnectionParam(data.projectId, data.config.connectionId); - const configuration = contexts.getContext(connectionFormConfigureContext); - - if (!data.resource.has(key)) { - return; - } - - await data.resource.load(key, configuration.connectionIncludes); +export class ConnectionFormState extends FormState { + constructor(serviceProvider: IServiceProvider, service: ConnectionFormService, config: IConnectionFormState) { + super(serviceProvider, service, config); } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Contexts/connectionConfigContext.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Contexts/connectionConfigContext.ts deleted file mode 100644 index 4e22566ca3..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Contexts/connectionConfigContext.ts +++ /dev/null @@ -1,12 +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 type { ConnectionConfig } from '@cloudbeaver/core-sdk'; - -export function connectionConfigContext(): ConnectionConfig { - return {}; -} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Contexts/connectionCredentialsStateContext.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Contexts/connectionCredentialsStateContext.ts deleted file mode 100644 index 62e656099e..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Contexts/connectionCredentialsStateContext.ts +++ /dev/null @@ -1,30 +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. - */ - -interface IConnectionCredentialsState { - authModelId: string | null; - networkHandlers: string[]; -} - -interface IConnectionCredentialsStateContext extends IConnectionCredentialsState { - requireAuthModel: (id: string) => void; - requireNetworkHandler: (id: string) => void; -} - -export function connectionCredentialsStateContext(): IConnectionCredentialsStateContext { - return { - authModelId: null, - networkHandlers: [], - requireAuthModel(id) { - this.authModelId = id; - }, - requireNetworkHandler(id) { - this.networkHandlers.push(id); - }, - }; -} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts index c509c4ec09..c425328345 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts @@ -1,103 +1,40 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, makeObservable } from 'mobx'; - -import { DBDriverResource } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { isObjectPropertyInfoStateEqual } from '@cloudbeaver/core-sdk'; -import { formStateContext } from '@cloudbeaver/core-ui'; -import { connectionFormConfigureContext } from '../connectionFormConfigureContext'; -import { ConnectionFormService } from '../ConnectionFormService'; -import { connectionConfigContext } from '../Contexts/connectionConfigContext'; -import type { IConnectionFormFillConfigData, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps'; -import { DriverPropertiesLoader } from './DriverPropertiesLoader'; +import { ConnectionFormService } from '../ConnectionFormService.js'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +const DriverProperties = importLazyComponent(() => import('./DriverProperties.js').then(m => m.DriverProperties)); -@injectable() +@injectable(() => [ConnectionFormService]) export class ConnectionDriverPropertiesTabService extends Bootstrap { - constructor(private readonly connectionFormService: ConnectionFormService, private readonly dbDriverResource: DBDriverResource) { + constructor(private readonly connectionFormService: ConnectionFormService) { super(); - - makeObservable(this, { - fillConfig: action, - }); } - register(): void { - this.connectionFormService.tabsContainer.add({ + override register(): void { + this.connectionFormService.parts.add({ key: 'driver_properties', - name: 'customConnection_properties', - title: 'customConnection_properties', + name: 'plugin_connections_connection_form_part_properties', + title: 'plugin_connections_connection_form_part_properties', order: 2, - panel: () => DriverPropertiesLoader, + panel: () => DriverProperties, isDisabled: (tabId, props) => { - if (props?.state.config.driverId) { - return !props.state.config.driverId; + const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null; + + if (optionsPart?.state.driverId) { + return false; } + return true; }, }); - - this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this)); - - this.connectionFormService.formStateTask.addHandler(this.formState.bind(this)); - - this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this)); - - this.connectionFormService.configureTask.addHandler(this.configure.bind(this)); - } - - load(): void {} - - private configure(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const configuration = contexts.getContext(connectionFormConfigureContext); - - configuration.include('includeProperties', 'includeProviderProperties'); - } - - private fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider) { - if (!updated) { - return; - } - if (!state.config.properties) { - state.config.properties = {}; - } - - if (!state.info) { - return; - } - - state.config.properties = { ...state.info.properties }; - } - - private prepareConfig({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - - config.properties = { ...state.config.properties }; - } - - private formState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - if (!data.info || !data.config.driverId) { - return; - } - - const config = contexts.getContext(connectionConfigContext); - const driver = this.dbDriverResource.get(data.config.driverId); - - if (!driver?.driverProperties) { - return; - } - - if (!isObjectPropertyInfoStateEqual(driver.driverProperties, config.properties, data.info.properties)) { - const stateContext = contexts.getContext(formStateContext); - - stateContext.markEdited(); - } } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionFormDriverPropertiesPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionFormDriverPropertiesPart.ts new file mode 100644 index 0000000000..963a8dc4c2 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionFormDriverPropertiesPart.ts @@ -0,0 +1,82 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { FormPart, type IFormState } from '@cloudbeaver/core-ui'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { ConnectionInfoPropertiesResource } from '@cloudbeaver/core-connections'; +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import type { IConnectionProperties } from '../Options/IConnectionConfig.js'; +import { runInAction, toJS } from 'mobx'; +import type { ConnectionFormOptionsPart } from '../Options/ConnectionFormOptionsPart.js'; + +function getDefaultState(): IConnectionProperties { + return {}; +} + +export class ConnectionFormDriverPropertiesPart extends FormPart { + constructor( + formState: IFormState, + private readonly connectionInfoPropertiesResource: ConnectionInfoPropertiesResource, + private readonly optionsPart: ConnectionFormOptionsPart, + ) { + super(formState, getDefaultState()); + + this.optionsPart.onDriverIdChange.addHandler(this.onDriverIdChangeHandler.bind(this)); + } + + private onDriverIdChangeHandler(driverId: string | undefined) { + this.reset(); + } + + override isOutdated(): boolean { + if (!this.optionsPart.connectionKey) { + return false; + } + + return this.connectionInfoPropertiesResource.isOutdated(this.optionsPart.connectionKey); + } + + protected override setState(state: Record): void { + super.setState(state); + this.optionsPart.state.properties = this.state; + } + + protected override setInitialState(initialState: Record): void { + super.setInitialState(initialState); + this.optionsPart.initialState.properties = initialState; + } + + protected override async loader(): Promise { + if (!this.optionsPart.connectionKey) { + this.setInitialState(getDefaultState()); + return; + } + + const connection = await this.connectionInfoPropertiesResource.load(this.optionsPart.connectionKey); + const properties = toJS(connection.properties); + + this.setInitialState(properties); + } + + protected override async saveChanges( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise {} + + protected override format( + data: IFormState, + contexts: IExecutionContextProvider>, + ): void | Promise { + runInAction(() => { + for (const key of Object.keys(this.state!)) { + if (typeof this.state[key] === 'string') { + this.state[key] = this.state[key].trim(); + } + } + }); + } +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.module.css b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.module.css new file mode 100644 index 0000000000..4c4a5ebc4f --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.module.css @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.coloredContainer { + flex: 1; + overflow: auto; +} + +.group { + max-height: 100%; +} + +.propertiesTable { + padding-top: 8px; + max-height: 100%; + box-sizing: border-box; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx index a24642225e..ce1c084aed 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,33 +8,22 @@ import { computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useMemo, useState } from 'react'; -import styled, { css } from 'reshadow'; -import { ColoredContainer, Group, IProperty, PropertiesTable, useResource, useStyles } from '@cloudbeaver/core-blocks'; +import { ColoredContainer, Group, type IProperty, PropertiesTable, s, useAutoLoad, useExecutor, useResource, useS } from '@cloudbeaver/core-blocks'; import { DBDriverResource } from '@cloudbeaver/core-connections'; -import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; +import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; import { uuid } from '@cloudbeaver/core-utils'; -import type { IConnectionFormProps } from '../IConnectionFormProps'; +import styles from './DriverProperties.module.css'; +import { getConnectionFormDriverPropertiesPart } from './getConnectionFormDriverPropertiesPart.js'; +import type { IConnectionFormProps } from '../IConnectionFormState.js'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; -const styles = css` - ColoredContainer { - flex: 1; - overflow: auto; - } - Group { - max-height: 100%; - } - PropertiesTable { - padding-top: 8px; - max-height: 100%; - box-sizing: border-box; - } -`; - -export const DriverProperties: TabContainerPanelComponent = observer(function DriverProperties({ tabId, state: formState }) { - const style = useStyles(styles); +export const DriverProperties: TabContainerPanelComponent = observer(function DriverProperties({ tabId, formState }) { const { selected } = useTab(tabId); + const style = useS(styles); + const driverPropertiesPart = getConnectionFormDriverPropertiesPart(formState); + const optionsPart = getConnectionFormOptionsPart(formState); const [state] = useState(() => { const propertiesList: IProperty[] = observable([]); @@ -53,22 +42,35 @@ export const DriverProperties: TabContainerPanelComponent propertiesList.splice(propertiesList.indexOf(property), 1); } - return { propertiesList, add, remove }; + function reset() { + propertiesList.splice(0, propertiesList.length); + } + + return { propertiesList, add, remove, reset }; + }); + + useExecutor({ + executor: optionsPart.onDriverIdChange, + handlers: [ + function handleDriverChange() { + state.reset(); + }, + ], }); const driver = useResource(DriverProperties, DBDriverResource, { - key: (selected && formState.config.driverId) || null, + key: (selected && optionsPart.state.driverId) || null, includes: ['includeDriverProperties'] as const, }); runInAction(() => { if (driver.data) { - for (const key of Object.keys(formState.config.properties)) { + for (const key of Object.keys(driverPropertiesPart.state)) { if (driver.data.driverProperties.some(property => property.id === key) || state.propertiesList.some(property => property.key === key)) { continue; } - state.add(key, formState.config.properties[key]); + state.add(key, driverPropertiesPart.state[key]); } } }); @@ -93,18 +95,21 @@ export const DriverProperties: TabContainerPanelComponent [driver.data], ); - return styled(style)( - - + useAutoLoad(DriverProperties, driverPropertiesPart, selected); + + return ( + + - , + ); }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverPropertiesLoader.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverPropertiesLoader.tsx deleted file mode 100644 index 0b0b4e632a..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverPropertiesLoader.tsx +++ /dev/null @@ -1,13 +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 React from 'react'; - -export const DriverPropertiesLoader = React.lazy(async () => { - const { DriverProperties } = await import('./DriverProperties'); - return { default: DriverProperties }; -}); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/getConnectionFormDriverPropertiesPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/getConnectionFormDriverPropertiesPart.ts new file mode 100644 index 0000000000..308aa4e6c4 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/getConnectionFormDriverPropertiesPart.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import type { IFormState } from '@cloudbeaver/core-ui'; +import { ConnectionFormDriverPropertiesPart } from './ConnectionFormDriverPropertiesPart.js'; +import { ConnectionInfoPropertiesResource } from '@cloudbeaver/core-connections'; +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; + +const DATA_CONTEXT_CONNECTION_FORM_DRIVER_PROPERTIES_PART = createDataContext( + 'Connection Form Driver Properties Part', +); + +export function getConnectionFormDriverPropertiesPart(formState: IFormState): ConnectionFormDriverPropertiesPart { + return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_DRIVER_PROPERTIES_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const connectionInfoPropertiesResource = di.getService(ConnectionInfoPropertiesResource); + const optionsPart = getConnectionFormOptionsPart(formState); + + return new ConnectionFormDriverPropertiesPart(formState, connectionInfoPropertiesResource, optionsPart); + }); +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts b/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts deleted file mode 100644 index 25e4a8b774..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts +++ /dev/null @@ -1,68 +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 type { ConnectionInfoResource, DatabaseConnection } from '@cloudbeaver/core-connections'; -import type { IExecutor, IExecutorHandlersCollection } from '@cloudbeaver/core-executor'; -import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; -import type { IFormStateInfo } from '@cloudbeaver/core-ui'; -import type { MetadataMap } from '@cloudbeaver/core-utils'; - -export type ConnectionFormMode = 'edit' | 'create'; -export type ConnectionFormType = 'admin' | 'public'; - -export interface IConnectionFormState { - mode: ConnectionFormMode; - type: ConnectionFormType; - projectId: string | null; - - config: ConnectionConfig; - - partsState: MetadataMap; - - readonly id: string; - readonly initError: Error | null; - readonly statusMessage: string | string[] | null; - readonly disabled: boolean; - readonly loading: boolean; - readonly configured: boolean; - - readonly availableDrivers: string[]; - readonly resource: ConnectionInfoResource; - readonly info: DatabaseConnection | undefined; - readonly readonly: boolean; - readonly submittingTask: IExecutorHandlersCollection; - readonly closeTask: IExecutor; - - readonly load: () => Promise; - readonly loadConnectionInfo: () => Promise; - readonly reset: () => void; - readonly setPartsState: (state: MetadataMap) => this; - readonly setOptions: (mode: ConnectionFormMode, type: ConnectionFormType) => this; - readonly setConfig: (projectId: string, config: ConnectionConfig) => this; - readonly setProject: (projectId: string) => this; - readonly setAvailableDrivers: (drivers: string[]) => this; - readonly save: () => Promise; - readonly test: () => Promise; - readonly checkFormState: () => Promise; - readonly close: () => Promise; - readonly dispose: () => void; -} - -export interface IConnectionFormProps { - state: IConnectionFormState; - onCancel?: () => void; -} - -export interface IConnectionFormFillConfigData { - updated: boolean; - state: IConnectionFormState; -} - -export interface IConnectionFormSubmitData { - submitType: 'submit' | 'test'; - state: IConnectionFormState; -} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormState.ts new file mode 100644 index 0000000000..f78fe13f4e --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormState.ts @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { schema } from '@cloudbeaver/core-utils'; +import type { IFormProps } from '@cloudbeaver/core-ui'; + +export const CONNECTION_FORM_STATE_SCHEMA = schema + .object({ + projectId: schema.string(), + availableDrivers: schema.array(schema.string()), + requiredNetworkHandlersIds: schema.array(schema.string()), + connectionId: schema.string().or(schema.undefined()), + type: schema.enum(['admin', 'public']), + }) + .strict(); + +export type IConnectionFormState = schema.infer; +export type IConnectionFormProps = IFormProps; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/AdvancedPropertiesForm.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/AdvancedPropertiesForm.tsx new file mode 100644 index 0000000000..c1ab3a2689 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/AdvancedPropertiesForm.tsx @@ -0,0 +1,37 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Container, Expandable, Group, ObjectPropertyInfoForm, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; +import { DBDriverExpertSettingsResource } from '@cloudbeaver/core-connections'; + +interface Props { + config: ConnectionConfig; + disabled?: boolean; + readonly?: boolean; +} + +export const AdvancedPropertiesForm = observer(function AdvancedPropertiesForm({ config, disabled, readonly }) { + const translate = useTranslate(); + const properties = useResource(AdvancedPropertiesForm, DBDriverExpertSettingsResource, config.driverId ?? null); + + if (!properties.data?.length) { + return null; + } + + return ( + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionFormOptionsPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionFormOptionsPart.ts new file mode 100644 index 0000000000..dfcb202dea --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionFormOptionsPart.ts @@ -0,0 +1,579 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { FormMode, FormPart, formSubmitContext, formValidationContext, type IFormState } from '@cloudbeaver/core-ui'; +import { DriverConfigurationType, type ConnectionConfig, type ObjectPropertyInfo, type TestConnectionMutation } from '@cloudbeaver/core-sdk'; +import { Executor, ExecutorInterrupter, type IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor'; +import { + ConnectionInfoAuthPropertiesResource, + ConnectionInfoCustomOptionsResource, + ConnectionInfoProjectKey, + ConnectionInfoProviderPropertiesResource, + ConnectionInfoResource, + createConnectionParam, + DatabaseAuthModelsResource, + DBDriverExpertSettingsResource, + DBDriverResource, + type ConnectionInfoAuthProperties, + type DBDriver, +} from '@cloudbeaver/core-connections'; +import type { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { action, computed, makeObservable, observable, reaction, toJS } from 'mobx'; +import { getUniqueName } from '@cloudbeaver/core-utils'; +import { getObjectPropertyDefaults } from '@cloudbeaver/core-blocks'; +import { isNotNullDefined } from '@dbeaver/js-helpers'; +import { parseJdbcUri } from '@dbeaver/jdbc-uri-parser'; + +import { getDefaultConfigurationType } from './getDefaultConfigurationType.js'; +import { getConnectionName } from './getConnectionName.js'; +import type { IConnectionFormOptionsState } from './IConnectionFormOptionsState.js'; +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import { ConnectionAuthenticationDialogLoader } from '../../ConnectionAuthentication/ConnectionAuthenticationDialogLoader.js'; + +const MAIN_PROPERTY_DATABASE_KEY = 'database'; +const MAIN_PROPERTY_HOST_KEY = 'host'; +const MAIN_PROPERTY_PORT_KEY = 'port'; +const MAIN_PROPERTY_SERVER_KEY = 'server'; + +const defaultStateGetter = (connectionId?: string, credentials?: Record) => + ({ + connectionId, + configurationType: DriverConfigurationType.Manual, + credentials: credentials ?? {}, + mainPropertyValues: {}, + expertSettingsValues: {}, + networkHandlersConfig: [], + providerProperties: {}, + }) as IConnectionFormOptionsState; + +export class ConnectionFormOptionsPart extends FormPart { + private disposeReaction: () => void; + + readonly onDriverIdChange: IExecutor; + + constructor( + formState: IFormState, + private readonly dbDriverResource: DBDriverResource, + private readonly projectInfoResource: ProjectInfoResource, + private readonly databaseAuthModelsResource: DatabaseAuthModelsResource, + private readonly connectionInfoResource: ConnectionInfoResource, + private readonly connectionInfoAuthPropertiesResource: ConnectionInfoAuthPropertiesResource, + private readonly connectionInfoCustomOptionsResource: ConnectionInfoCustomOptionsResource, + private readonly connectionInfoProviderPropertiesResource: ConnectionInfoProviderPropertiesResource, + private readonly localizationService: LocalizationService, + private readonly commonDialogService: CommonDialogService, + private readonly notificationService: NotificationService, + private readonly dbDriverExpertSettingsResource: DBDriverExpertSettingsResource, + ) { + super(formState, defaultStateGetter(formState.state.connectionId)); + + this.onDriverIdChange = new Executor(); + + this.disposeReaction = reaction( + () => this.getNameTemplate(), + (value, prev) => { + if (this.formState.mode === 'edit') { + return; + } + if (!this.state.name || prev === this.state.name) { + this.state.name = value; + } + }, + ); + + makeObservable(this, { + setAuthModelId: action.bound, + setDriverId: action.bound, + connectionKey: computed, + askCredentials: action.bound, + }); + } + + private async askCredentials(state: IConnectionFormOptionsState): Promise { + const driver = this.state.driverId ? this.dbDriverResource.get(this.state.driverId) : undefined; + const isCredentialsRequired = !state.saveCredentials || this.formState.state.requiredNetworkHandlersIds.length; + + if (!isCredentialsRequired || driver?.anonymousAccess) { + return state; + } + + const stateToRestore: IConnectionFormOptionsState = {}; + + // does not show credentials in dialog if they are already saved + if (state.saveCredentials) { + if (state.authModelId) { + stateToRestore.authModelId = state.authModelId; + delete state.authModelId; + } + + if (state.credentials) { + stateToRestore.credentials = toJS(state.credentials); + delete state.credentials; + } + } + + // does not show network handlers in dialog if they are not required + if (!this.formState.state.requiredNetworkHandlersIds.length && state.networkHandlersConfig) { + stateToRestore.networkHandlersConfig = toJS(state.networkHandlersConfig); + delete state.networkHandlersConfig; + } + + const result = await this.commonDialogService.open(ConnectionAuthenticationDialogLoader, { + config: state, + authModelId: state.authModelId ?? null, + networkHandlers: this.formState.state.requiredNetworkHandlersIds, + projectId: this.formState.state.projectId, + }); + + if (result === DialogueStateResult.Rejected) { + return null; + } + + for (const key of Object.keys(stateToRestore)) { + state[key as keyof IConnectionFormOptionsState] = observable(stateToRestore[key as keyof IConnectionFormOptionsState] as any); + } + + return state; + } + + get connectionKey() { + if (!this.initialState.connectionId || !this.formState.state.projectId) { + return null; + } + + return createConnectionParam(this.formState.state.projectId, this.initialState.connectionId); + } + + // do not check outdated of userInfoResource cause it synced with projectInfoResource which is handled in optionsPart outdated method + // otherwise you would get an infinite loading of the form + override isOutdated(): boolean { + if (!this.formState.state.projectId) { + return false; + } + + if (this.formState.mode === 'create') { + if (this.state.driverId && this.dbDriverResource.isOutdated(this.state.driverId)) { + return true; + } + } + + if (!this.connectionKey) { + return false; + } + + const isAuthPropertiesOutdated = this.connectionInfoAuthPropertiesResource.isOutdated(this.connectionKey); + const isCustomOptionsOutdated = this.connectionInfoCustomOptionsResource.isOutdated(this.connectionKey); + const isProviderPropertiesOutdated = this.connectionInfoProviderPropertiesResource.isOutdated(this.connectionKey); + + return isAuthPropertiesOutdated || isCustomOptionsOutdated || isProviderPropertiesOutdated; + } + + protected override async loader(): Promise { + if (this.formState.mode === 'create') { + const credentials = this.state.authModelId + ? getObjectPropertyDefaults(await this.getConnectionAuthModelProperties(this.state.authModelId)) + : undefined; + + this.setInitialState(defaultStateGetter(this.initialState.connectionId ?? this.formState.state.connectionId, credentials)); + + await this.setDriverId(this.state.driverId); + + return; + } + + if (!this.connectionKey) { + console.error('Connection connection key should be defined'); + return; + } + + const [authPropertiesInfo, customOptionsInfo, providerPropertiesInfo] = await Promise.all([ + this.connectionInfoAuthPropertiesResource.load(this.connectionKey), + this.connectionInfoCustomOptionsResource.load(this.connectionKey), + this.connectionInfoProviderPropertiesResource.load(this.connectionKey), + ]); + + const config: ConnectionConfig = defaultStateGetter(); + + config.connectionId = customOptionsInfo.id; + config.configurationType = customOptionsInfo.configurationType; + + config.name = customOptionsInfo.name; + config.description = customOptionsInfo.description; + config.driverId = customOptionsInfo.driverId; + + config.host = customOptionsInfo.host || customOptionsInfo.mainPropertyValues?.[MAIN_PROPERTY_HOST_KEY]; + config.port = customOptionsInfo.port || customOptionsInfo.mainPropertyValues?.[MAIN_PROPERTY_PORT_KEY]; + config.serverName = customOptionsInfo.serverName || customOptionsInfo.mainPropertyValues?.[MAIN_PROPERTY_SERVER_KEY]; + config.databaseName = customOptionsInfo.databaseName || customOptionsInfo.mainPropertyValues?.[MAIN_PROPERTY_DATABASE_KEY]; + + config.url = customOptionsInfo.url; + config.folder = customOptionsInfo.folder; + + config.authModelId = authPropertiesInfo.authModel; + config.saveCredentials = authPropertiesInfo.credentialsSaved; + config.sharedCredentials = authPropertiesInfo.sharedCredentials; + + if (authPropertiesInfo.authProperties) { + for (const property of authPropertiesInfo.authProperties) { + if (!property.features.includes('password')) { + config.credentials[property.id!] = property.value; + } + } + } + + if (providerPropertiesInfo.providerProperties) { + config.providerProperties = { ...toJS(providerPropertiesInfo.providerProperties) }; + } + + if (customOptionsInfo.mainPropertyValues) { + config.mainPropertyValues = { ...toJS(customOptionsInfo.mainPropertyValues) }; + } + + if (customOptionsInfo.expertSettingsValues) { + config.expertSettingsValues = toJS(customOptionsInfo.expertSettingsValues); + } + + this.formState.state.availableDrivers = [customOptionsInfo.driverId]; + + this.setInitialState(config); + } + + private getNameTemplate() { + const driver = this.state.driverId ? this.dbDriverResource.get(this.state.driverId) : undefined; + + if (this.state.configurationType === DriverConfigurationType.Url) { + const parsedJdbcUri = parseJdbcUri(this.state.url ?? ''); + return getConnectionName({ + driverName: driver?.name || 'New connection', + host: parsedJdbcUri.host, + port: parsedJdbcUri.port, + defaultPort: driver?.defaultPort, + }); + } + + return getConnectionName({ + driverName: driver?.name || 'New connection', + host: this.state.host, + port: this.state.port, + defaultPort: driver?.defaultPort, + }); + } + + async setDriverId(driverId: string | undefined): Promise { + if (this.formState.mode === 'edit') { + return; + } + + let driver: DBDriver | undefined; + let prevDriver: DBDriver | undefined; + const prevDriverId = this.state.driverId; + this.state.driverId = driverId; + + if (driverId) { + driver = await this.dbDriverResource.load(driverId, ['includeProviderProperties']); + } + + if (!driver) { + return; + } + + if (prevDriverId) { + prevDriver = await this.dbDriverResource.load(prevDriverId, ['includeProviderProperties']); + } + + if (!this.state.configurationType || !driver?.configurationTypes.includes(this.state.configurationType)) { + this.state.configurationType = getDefaultConfigurationType(driver); + } + + if ((!prevDriver && this.state.host === undefined) || this.state.host === prevDriver?.defaultServer) { + this.state.host = driver?.defaultServer || 'localhost'; + } + + if ((!prevDriver && this.state.port === undefined) || this.state.port === prevDriver?.defaultPort) { + this.state.port = driver?.defaultPort; + } + + if ((!prevDriver && this.state.databaseName === undefined) || this.state.databaseName === prevDriver?.defaultDatabase) { + this.state.databaseName = driver?.defaultDatabase; + } + + if ((!prevDriver && this.state.url === undefined) || this.state.url === prevDriver?.sampleURL) { + this.state.url = driver?.sampleURL; + } + + if (driver?.id !== prevDriver?.id) { + this.state.credentials = {}; + this.state.providerProperties = {}; + this.state.expertSettingsValues = {}; + + await this.setAuthModelId(driver?.defaultAuthModel); + await this.onDriverIdChange.execute(this.state.driverId); + } + } + + async setAuthModelId(modelId: string | undefined): Promise { + if (modelId === this.initialState.authModelId) { + this.state.credentials = { ...this.initialState.credentials }; + } else if (modelId !== this.state.authModelId) { + const properties = modelId ? await this.getConnectionAuthModelProperties(modelId) : []; + this.state.credentials = getObjectPropertyDefaults(properties); + } + + this.state.authModelId = modelId; + } + + protected override async format( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise { + if (!this.state.driverId || !this.formState.state.projectId) { + return; + } + + const [driver, expertSettings] = await Promise.all([ + this.dbDriverResource.load(this.state.driverId, ['includeProviderProperties', 'includeMainProperties']), + this.dbDriverExpertSettingsResource.load(this.state.driverId), + ]); + + this.formState.state.requiredNetworkHandlersIds = observable([]); + this.state.networkHandlersConfig = observable([]); + + this.state.name = this.state.name?.trim(); + this.state.description = this.state.description?.trim(); + + if (!this.state.folder) { + delete this.state.folder; + } + + if (this.state.configurationType === DriverConfigurationType.Url) { + this.state.url = this.state.url?.trim(); + } else { + // if manual type configuration set, it helps to keep host, port, etc. properties (not saved on backend) + delete this.state.url; + } + + // databaseName, host, port, serverName only saves on backend like this + if (this.state.configurationType === DriverConfigurationType.Manual && !driver.useCustomPage) { + this.state.mainPropertyValues![MAIN_PROPERTY_DATABASE_KEY] = this.state.databaseName?.trim(); + + if (!driver.embedded) { + this.state.mainPropertyValues![MAIN_PROPERTY_HOST_KEY] = this.state.host?.trim(); + this.state.mainPropertyValues![MAIN_PROPERTY_PORT_KEY] = this.state.port?.trim(); + } + + if (driver.requiresServerName) { + this.state.mainPropertyValues![MAIN_PROPERTY_SERVER_KEY] = this.state.serverName?.trim(); + } + } + + if ((this.state.authModelId || driver.defaultAuthModel) && !driver.anonymousAccess) { + this.state.authModelId = this.state.authModelId || driver.defaultAuthModel; + this.state.saveCredentials = this.state.saveCredentials || this.state.sharedCredentials; + const authPropertiesInfo = this.connectionKey ? await this.connectionInfoAuthPropertiesResource.load(this.connectionKey) : undefined; + + const properties = await this.getConnectionAuthModelProperties(this.state.authModelId, authPropertiesInfo); + const passwordProperty = properties.find(property => property.features.includes('password')); + const isPasswordEmpty = + passwordProperty && + (this.state.credentials?.[passwordProperty.id!] === passwordProperty.defaultValue || !this.state.credentials?.[passwordProperty.id!]); + + if (isCredentialsChanged(properties, this.state.credentials!)) { + this.state.credentials = prepareDynamicProperties(properties, toJS(this.state.credentials!)); + } + + if (isPasswordEmpty) { + delete this.state.credentials?.[passwordProperty.id!]; + } + } + + if (driver.providerProperties.length > 0) { + this.state.providerProperties = prepareDynamicProperties( + driver.providerProperties, + toJS(this.state.providerProperties!), + this.state.configurationType, + ); + } + + if (driver.useCustomPage && driver.mainProperties.length > 0) { + this.state.mainPropertyValues = prepareDynamicProperties(driver.mainProperties, this.state.mainPropertyValues!, this.state.configurationType); + } + + if (expertSettings.length > 0) { + this.state.expertSettingsValues = prepareDynamicProperties(expertSettings, this.state.expertSettingsValues!); + } + } + + private async getConnectionAuthModelProperties(authModelId: string, connectionInfo?: ConnectionInfoAuthProperties): Promise { + const authModel = await this.databaseAuthModelsResource.load(authModelId); + + let properties = authModel.properties; + + if (connectionInfo?.authProperties && connectionInfo.authProperties.length > 0) { + properties = connectionInfo.authProperties; + } + + return properties; + } + + private getTestMessageInfo(testContext: TestConnectionMutation['connection']) { + let message = ''; + + if (testContext.clientVersion) { + message += this.localizationService.translate('plugin_connections_connection_client_version', undefined, { + version: testContext.clientVersion, + }); + } + + if (testContext.serverVersion) { + message += this.localizationService.translate('plugin_connections_connection_server_version', undefined, { + version: testContext.serverVersion, + }); + } + + if (testContext.connectTime) { + message += this.localizationService.translate('plugin_connections_connection_connection_time', undefined, { + time: testContext.connectTime, + }); + } + + return message; + } + + protected override async validate( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise { + const validation = contexts.getContext(formValidationContext); + + if (this.state.configurationType === DriverConfigurationType.Manual && this.state.host?.length === 0 && this.state.driverId) { + const driver = await this.dbDriverResource.load(this.state.driverId); + if (!driver.embedded) { + validation.error('plugin_connections_connection_form_host_invalid'); + } + } + + if (!this.state.name?.length) { + validation.error('plugin_connections_connection_form_name_invalid'); + } + + if (this.state.driverId && this.state.configurationType) { + const driver = await this.dbDriverResource.load(this.state.driverId, ['includeProviderProperties']); + + if (!driver.configurationTypes.includes(this.state.configurationType)) { + validation.error('plugin_connections_connection_form_host_configuration_invalid'); + } + } + + if (this.formState.state.projectId !== null && this.formState.mode === 'create') { + const project = this.projectInfoResource.get(this.formState.state.projectId); + + if (!project?.canEditDataSources) { + validation.error('plugin_connections_connection_form_project_invalid'); + } + } + } + + protected override async saveChanges( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise { + if (!this.formState.state.projectId) { + return; + } + + const submitType = contexts.getContext(formSubmitContext); + if (submitType.type === 'submit') { + if (this.formState.mode === 'edit') { + await this.connectionInfoResource.update(this.connectionKey!, this.state); + } else { + const connections = await this.connectionInfoResource.load(ConnectionInfoProjectKey(this.formState.state.projectId)); + const connectionNames = connections.map(connection => connection.name); + + const uniqueName = getUniqueName(this.state.name || '', connectionNames); + const connection = await this.connectionInfoResource.create(this.formState.state.projectId, { ...this.state, name: uniqueName }); + this.state.connectionId = connection.id; + this.initialState.connectionId = connection.id; + this.formState.setMode(FormMode.Edit); + } + } else { + try { + const stateCopy = await this.askCredentials(observable(toJS(this.state))); + + if (!stateCopy) { + return; + } + + const info = await this.connectionInfoResource.test(this.formState.state.projectId, stateCopy); + + this.notificationService.logSuccess({ + title: 'plugin_connections_connection_established', + message: this.getTestMessageInfo(info), + }); + } catch (error) { + this.notificationService.logException(error as Error, 'connections_connection_test_fail'); + } finally { + // to prevent form from resetting the state after saving + ExecutorInterrupter.interrupt(contexts); + } + } + } + + override dispose(): void { + this.disposeReaction(); + } +} + +function prepareDynamicProperties( + propertiesInfo: ObjectPropertyInfo[], + properties: Record, + configurationType?: DriverConfigurationType, +) { + const result: Record = { ...properties }; + + for (const propertyInfo of propertiesInfo) { + if (!propertyInfo.id) { + continue; + } + + const supported = configurationType === undefined || propertyInfo.supportedConfigurationTypes?.some(type => type === configurationType); + + if (!supported) { + delete result[propertyInfo.id]; + } else { + const isDefault = isNotNullDefined(propertyInfo.defaultValue); + if (!(propertyInfo.id in result) && isDefault) { + result[propertyInfo.id] = propertyInfo.defaultValue; + } + } + } + + for (const key of Object.keys(result)) { + if (typeof result[key] === 'string') { + result[key] = result[key]?.trim(); + } + } + + return result; +} + +function isCredentialsChanged(authProperties: ObjectPropertyInfo[], credentials: Record) { + for (const property of authProperties) { + const value = credentials[property.id!]; + + if (property.features.includes('password')) { + if (value !== undefined) { + return property.features.includes('file') ? true : !!value; + } + } else if (value !== property.value) { + return true; + } + } + return false; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts index a604d0cbf2..f3c6bafb80 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts @@ -1,423 +1,33 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, makeObservable, runInAction, toJS } from 'mobx'; import React from 'react'; -import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, UserInfoResource } from '@cloudbeaver/core-authentication'; -import { - ConnectionInfoProjectKey, - createConnectionParam, - DatabaseAuthModelsResource, - DatabaseConnection, - DBDriverResource, - isJDBCConnection, -} from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { LocalizationService } from '@cloudbeaver/core-localization'; -import { isSharedProject, ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { DriverConfigurationType, isObjectPropertyInfoStateEqual, ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; -import { formStateContext } from '@cloudbeaver/core-ui'; -import { getUniqueName, isNotNullDefined, isValuesEqual } from '@cloudbeaver/core-utils'; -import { connectionFormConfigureContext } from '../connectionFormConfigureContext'; -import { ConnectionFormService } from '../ConnectionFormService'; -import { connectionConfigContext } from '../Contexts/connectionConfigContext'; -import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext'; -import type { IConnectionFormFillConfigData, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps'; -import { getConnectionName } from './getConnectionName'; +import { ConnectionFormService } from '../ConnectionFormService.js'; export const Options = React.lazy(async () => { - const { Options } = await import('./Options'); + const { Options } = await import('./Options.js'); return { default: Options }; }); -@injectable() +@injectable(() => [ConnectionFormService]) export class ConnectionOptionsTabService extends Bootstrap { - constructor( - private readonly serverConfigResource: ServerConfigResource, - private readonly projectInfoResource: ProjectInfoResource, - private readonly connectionFormService: ConnectionFormService, - private readonly dbDriverResource: DBDriverResource, - private readonly userInfoResource: UserInfoResource, - private readonly localizationService: LocalizationService, - private readonly authProvidersResource: AuthProvidersResource, - private readonly databaseAuthModelsResource: DatabaseAuthModelsResource, - ) { + constructor(private readonly connectionFormService: ConnectionFormService) { super(); - - makeObservable(this, { - fillConfig: action, - }); } - register(): void { - this.connectionFormService.tabsContainer.add({ + override register(): void { + this.connectionFormService.parts.add({ key: 'options', - name: 'customConnection_options', + name: 'plugin_connections_connection_form_part_main', order: 1, panel: () => Options, }); - - this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this)); - - this.connectionFormService.formValidationTask.addHandler(this.validate.bind(this)); - - this.connectionFormService.formSubmittingTask.addHandler(this.save.bind(this)); - - this.connectionFormService.formStateTask.addHandler(this.formState.bind(this)).addHandler(this.formAuthState.bind(this)); - - this.connectionFormService.configureTask.addHandler(this.configure.bind(this)); - - this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this)); - } - - load(): void {} - - isProjectShared(state: IConnectionFormState): boolean { - if (state.projectId === null) { - return false; - } - - const project = this.projectInfoResource.get(state.projectId); - - if (!project) { - return false; - } - - return isSharedProject(project); - } - - private async save({ state, submitType }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const status = contexts.getContext(this.connectionFormService.connectionStatusContext); - const config = contexts.getContext(connectionConfigContext); - - if (!state.projectId) { - status.error('connections_connection_create_fail'); - return; - } - - try { - if (submitType === 'submit') { - if (state.mode === 'edit') { - const connection = await state.resource.update(createConnectionParam(state.projectId, config.connectionId!), config); - status.info('Connection was updated'); - status.info(connection.name); - } else { - const connection = await state.resource.create(state.projectId, config); - config.connectionId = connection.id; - status.info('Connection was created'); - status.info(connection.name); - } - } else { - const info = await state.resource.test(state.projectId, config); - status.info('Connection is established'); - status.info('Client version: ' + info.clientVersion); - status.info('Server version: ' + info.serverVersion); - status.info('Connection time: ' + info.connectTime); - } - } catch (exception: any) { - if (submitType === 'submit') { - status.error('connections_connection_create_fail', exception); - } else { - status.error('connections_connection_test_fail', exception); - } - } - } - - private async validate({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const validation = contexts.getContext(this.connectionFormService.connectionValidationContext); - - if (state.config.configurationType === DriverConfigurationType.Manual && state.config.host?.length === 0 && state.config.driverId) { - const driver = await this.dbDriverResource.load(state.config.driverId); - if (!driver.embedded) { - validation.error('plugin_connections_connection_form_host_invalid'); - } - } - - if (!state.config.name?.length) { - validation.error('plugin_connections_connection_form_name_invalid'); - } - - if (state.config.driverId && state.config.configurationType) { - const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties']); - - if (!driver.configurationTypes.includes(state.config.configurationType)) { - validation.error('plugin_connections_connection_form_host_configuration_invalid'); - } - } - - if (state.projectId !== null && state.mode === 'create') { - const project = this.projectInfoResource.get(state.projectId); - - if (!project?.canEditDataSources) { - validation.error('plugin_connections_connection_form_project_invalid'); - } - } - - // if (state.config.folder && !state.config.folder.match(CONNECTION_FOLDER_NAME_VALIDATION)) { - // validation.error('connections_connection_folder_validation'); - // } - } - - private async setDefaults(state: IConnectionFormState) { - if (!state.config.driverId) { - return; - } - - const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties']); - - state.config.authModelId = driver?.defaultAuthModel; - - state.config.configurationType = driver?.configurationTypes.includes(DriverConfigurationType.Manual) - ? DriverConfigurationType.Manual - : DriverConfigurationType.Url; - - state.config.host = driver?.defaultServer || 'localhost'; - state.config.port = driver?.defaultPort; - state.config.databaseName = driver?.defaultDatabase; - state.config.url = driver?.sampleURL; - - if (isJDBCConnection(driver)) { - state.config.name = state.config.url; - } else { - state.config.name = getConnectionName(driver.name || '', state.config.host, state.config.port, driver.defaultPort); - } - } - - private async fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider) { - if (!updated) { - return; - } - - if (!state.config.credentials || updated) { - state.config.credentials = {}; - state.config.saveCredentials = false; - } - - if (!state.config.providerProperties || updated) { - state.config.providerProperties = {}; - } - - if (!state.info) { - await this.setDefaults(state); - return; - } - - state.config.connectionId = state.info.id; - state.config.configurationType = state.info.configurationType; - - state.config.name = state.info.name; - state.config.description = state.info.description; - state.config.template = state.info.template; - state.config.driverId = state.info.driverId; - - state.config.host = state.info.host; - state.config.port = state.info.port; - state.config.serverName = state.info.serverName; - state.config.databaseName = state.info.databaseName; - state.config.url = state.info.url; - state.config.folder = state.info.folder; - - state.config.authModelId = state.info.authModel; - state.config.saveCredentials = state.info.credentialsSaved; - state.config.sharedCredentials = state.info.sharedCredentials; - - if (state.info.authProperties) { - for (const property of state.info.authProperties) { - if (!property.features.includes('password')) { - state.config.credentials[property.id!] = property.value; - } - } - } - - if (state.info.providerProperties) { - state.config.providerProperties = { ...state.info.providerProperties }; - } - } - - private configure(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const configuration = contexts.getContext(connectionFormConfigureContext); - - configuration.include('includeOrigin', 'includeAuthProperties', 'includeCredentialsSaved', 'customIncludeOptions'); - } - - private async prepareConfig({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - const credentialsState = contexts.getContext(connectionCredentialsStateContext); - - if (!state.config.driverId || !state.projectId) { - return; - } - - const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties']); - const tempConfig = toJS(config); - - if (state.mode === 'edit') { - tempConfig.connectionId = state.config.connectionId; - } - - tempConfig.configurationType = state.config.configurationType; - - tempConfig.name = state.config.name?.trim(); - - if (tempConfig.name && state.mode === 'create') { - const connections = await state.resource.load(ConnectionInfoProjectKey(state.projectId)); - const connectionNames = connections.map(connection => connection.name); - - tempConfig.name = getUniqueName(tempConfig.name, connectionNames); - } - - tempConfig.description = state.config.description; - - tempConfig.template = state.config.template; - - tempConfig.driverId = state.config.driverId; - - if (!state.config.template && state.config.folder) { - tempConfig.folder = state.config.folder; - } - - if (tempConfig.configurationType === DriverConfigurationType.Url) { - tempConfig.url = state.config.url; - } else { - if (!driver.embedded) { - tempConfig.host = state.config.host; - tempConfig.port = state.config.port; - } - if (driver.requiresServerName) { - tempConfig.serverName = state.config.serverName; - } - tempConfig.databaseName = state.config.databaseName; - } - - if ((state.config.authModelId || driver.defaultAuthModel) && !driver.anonymousAccess) { - tempConfig.authModelId = state.config.authModelId || driver.defaultAuthModel; - tempConfig.saveCredentials = state.config.saveCredentials; - tempConfig.sharedCredentials = state.config.sharedCredentials; - - const properties = await this.getConnectionAuthModelProperties(tempConfig.authModelId, state.info); - - if (this.isCredentialsChanged(properties, state.config.credentials)) { - tempConfig.credentials = { ...state.config.credentials }; - } - - if (!tempConfig.saveCredentials) { - credentialsState.requireAuthModel(tempConfig.authModelId); - } - } - - if (driver.providerProperties.length > 0) { - const providerProperties: Record = { ...state.config.providerProperties }; - - for (const providerProperty of driver.providerProperties) { - if (!providerProperty.id) { - continue; - } - - const supported = providerProperty.supportedConfigurationTypes?.some(t => t === tempConfig.configurationType); - - if (!supported) { - delete providerProperties[providerProperty.id]; - } else { - const isDefault = isNotNullDefined(providerProperty.defaultValue); - if (!(providerProperty.id in providerProperties) && isDefault) { - providerProperties[providerProperty.id] = providerProperty.defaultValue; - } - } - } - - tempConfig.providerProperties = providerProperties; - } - - runInAction(() => { - Object.assign(config, tempConfig); - }); - } - - private async formAuthState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - const stateContext = contexts.getContext(formStateContext); - - const driver = await this.dbDriverResource.load(config.driverId!, ['includeProviderProperties']); - const authModel = await this.databaseAuthModelsResource.load(config.authModelId ?? data.info?.authModel ?? driver.defaultAuthModel); - - const providerId = authModel.requiredAuth ?? data.info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID; - - await this.userInfoResource.load(); - - if (!this.userInfoResource.hasToken(providerId)) { - const provider = await this.authProvidersResource.load(providerId); - const message = this.localizationService.translate('connections_public_connection_cloud_auth_required', undefined, { - providerLabel: provider.label, - }); - stateContext.setInfo(message); - stateContext.readonly = data.mode === 'edit'; - } - } - - private async formState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - if (!data.info) { - return; - } - - const config = contexts.getContext(connectionConfigContext); - const stateContext = contexts.getContext(formStateContext); - const driver = await this.dbDriverResource.load(data.config.driverId!, ['includeProviderProperties']); - - if ( - !isValuesEqual(config.name, data.info.name, '') || - !isValuesEqual(config.configurationType, data.info.configurationType, DriverConfigurationType.Manual) || - !isValuesEqual(config.description, data.info.description, '') || - !isValuesEqual(config.template, data.info.template, true) || - !isValuesEqual(config.folder, data.info.folder, undefined) || - !isValuesEqual(config.driverId, data.info.driverId, '') || - (config.url !== undefined && !isValuesEqual(config.url, data.info.url, '')) || - (config.host !== undefined && !isValuesEqual(config.host, data.info.host, '')) || - (config.port !== undefined && !isValuesEqual(config.port, data.info.port, '')) || - (config.serverName !== undefined && !isValuesEqual(config.serverName, data.info.serverName, '')) || - (config.databaseName !== undefined && !isValuesEqual(config.databaseName, data.info.databaseName, '')) || - config.credentials !== undefined || - (config.authModelId !== undefined && !isValuesEqual(config.authModelId, data.info.authModel, '')) || - (config.saveCredentials !== undefined && config.saveCredentials !== data.info.credentialsSaved) || - (config.sharedCredentials !== undefined && config.sharedCredentials !== data.info.sharedCredentials) || - (config.providerProperties !== undefined && - !isObjectPropertyInfoStateEqual(driver.providerProperties, config.providerProperties, data.info.providerProperties)) - ) { - stateContext.markEdited(); - } - } - - private isCredentialsChanged(authProperties: ObjectPropertyInfo[], credentials: Record) { - for (const property of authProperties) { - const value = credentials[property.id!]; - - if (property.features.includes('password')) { - if (value !== undefined) { - return property.features.includes('file') ? true : !!value; - } - } else if (value !== property.value) { - return true; - } - } - return false; - } - - private async getConnectionAuthModelProperties(authModelId: string, connectionInfo?: DatabaseConnection): Promise { - const authModel = await this.databaseAuthModelsResource.load(authModelId); - - let properties = authModel.properties; - - if (connectionInfo?.authProperties && connectionInfo.authProperties.length > 0) { - properties = connectionInfo.authProperties; - } - - return properties; } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionConfig.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionConfig.ts new file mode 100644 index 0000000000..4ea42307b1 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionConfig.ts @@ -0,0 +1,41 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { DriverConfigurationType } from '@cloudbeaver/core-sdk'; +import { schema } from '@cloudbeaver/core-utils'; +import { CONNECTION_NETWORK_HANDLER_SCHEMA } from './IConnectionNetworkHanler.js'; + +export const CONNECTION_PROPERTIES_SCHEMA = schema.record(schema.string(), schema.any()); + +export const CONNECTION_CONFIG_SCHEMA = schema.object({ + authModelId: schema.string().optional(), + configurationType: schema.enum([DriverConfigurationType.Manual, DriverConfigurationType.Url]).optional(), + connectionId: schema.string().optional(), + credentials: schema.record(schema.string(), schema.any()).optional(), + dataSourceId: schema.string().optional(), + databaseName: schema.string().optional(), + description: schema.string().optional(), + driverId: schema.string().optional(), + folder: schema.string().optional(), + host: schema.string().optional(), + mainPropertyValues: schema.record(schema.string(), schema.any()).optional(), + expertSettingsValues: schema.record(schema.string(), schema.any()).optional(), + name: schema.string().optional(), + networkHandlersConfig: schema.array(CONNECTION_NETWORK_HANDLER_SCHEMA).optional(), + port: schema.string().optional(), + properties: CONNECTION_PROPERTIES_SCHEMA.optional(), + providerProperties: schema.record(schema.string(), schema.any()).optional(), + saveCredentials: schema.boolean().optional(), + selectedSecretId: schema.string().optional(), + serverName: schema.string().optional(), + sharedCredentials: schema.boolean().optional(), + url: schema.string().optional(), + userName: schema.string().optional(), + userPassword: schema.string().optional(), +}); + +export type IConnectionProperties = schema.infer; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionFormOptionsState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionFormOptionsState.ts new file mode 100644 index 0000000000..1283a2f4fb --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionFormOptionsState.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { schema } from '@cloudbeaver/core-utils'; +import { CONNECTION_CONFIG_SCHEMA } from './IConnectionConfig.js'; + +export const CONNECTION_FORM_OPTIONS_SCHEMA = CONNECTION_CONFIG_SCHEMA.extend({}); + +export type IConnectionFormOptionsState = schema.infer; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionNetworkHanler.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionNetworkHanler.ts new file mode 100644 index 0000000000..3747bd9259 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionNetworkHanler.ts @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { NetworkHandlerAuthType } from '@cloudbeaver/core-sdk'; +import { schema } from '@cloudbeaver/core-utils'; + +export const CONNECTION_NETWORK_HANDLER_SCHEMA = schema.object({ + id: schema.string(), + authType: schema.nativeEnum(NetworkHandlerAuthType).optional(), + enabled: schema.boolean().optional(), + key: schema.string().optional(), + password: schema.string().optional(), + properties: schema.record(schema.string(), schema.any()).optional(), + savePassword: schema.boolean().optional(), + secureProperties: schema.record(schema.string(), schema.any()).optional(), + userName: schema.string().optional(), +}); + +export type INetworkHandlerConfig = schema.infer; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.module.css b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.module.css new file mode 100644 index 0000000000..afbf301b65 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.form { + flex: 1; + overflow: auto; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx index ecec62ab9c..96e0a4a4ae 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx @@ -1,16 +1,16 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useCallback, useRef } from 'react'; -import styled, { css } from 'reshadow'; +import { useContext, useRef } from 'react'; -import { AUTH_PROVIDER_LOCAL_ID, EAdminPermission } from '@cloudbeaver/core-authentication'; +import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; import { + Alert, ColoredContainer, Combobox, Container, @@ -21,268 +21,258 @@ import { Group, GroupTitle, InputField, + Link, ObjectPropertyInfoForm, Radio, RadioGroup, + s, Textarea, useAdministrationSettings, useFormValidator, usePermission, useResource, + useS, useTranslate, + useAuthenticationAction, + useAutoLoad, + Text, } from '@cloudbeaver/core-blocks'; -import { DatabaseAuthModelsResource, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections'; +import { + ConnectionInfoAuthPropertiesResource, + ConnectionInfoOriginResource, + DatabaseAuthModelsResource, + type DBDriver, + DBDriverResource, + isLocalConnection, +} from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; -import { CachedResourceListEmptyKey, resourceKeyList } from '@cloudbeaver/core-resource'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { EAdminPermission, ServerConfigResource } from '@cloudbeaver/core-root'; import { DriverConfigurationType } from '@cloudbeaver/core-sdk'; -import { type TabContainerPanelComponent, useAuthenticationAction } from '@cloudbeaver/core-ui'; -import { isSafari } from '@cloudbeaver/core-utils'; +import { type TabContainerPanelComponent, TabsContext } from '@cloudbeaver/core-ui'; +import { EMPTY_ARRAY } from '@cloudbeaver/core-utils'; import { ProjectSelect } from '@cloudbeaver/plugin-projects'; -import { ConnectionFormService } from '../ConnectionFormService'; -import type { IConnectionFormProps } from '../IConnectionFormProps'; -import { ConnectionOptionsTabService } from './ConnectionOptionsTabService'; -import { ParametersForm } from './ParametersForm'; -import { ProviderPropertiesForm } from './ProviderPropertiesForm'; -import { useOptions } from './useOptions'; +import { ConnectionAuthModelCredentialsForm } from '../ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm.js'; +import { ConnectionAuthModelSelector } from '../ConnectionAuthModelCredentials/ConnectionAuthModelSelector.js'; +import { CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID } from '../SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID.js'; +import { AdvancedPropertiesForm } from './AdvancedPropertiesForm.js'; +import styles from './Options.module.css'; +import { ParametersForm } from './ParametersForm.js'; +import { ProviderPropertiesForm } from './ProviderPropertiesForm.js'; +import { getConnectionFormOptionsPart } from './getConnectionFormOptionsPart.js'; +import type { IConnectionFormProps } from '../IConnectionFormState.js'; const PROFILE_AUTH_MODEL_ID = 'profile'; -const styles = css` - Form { - flex: 1; - overflow: auto; - } -`; - interface IDriverConfiguration { name: string; value: DriverConfigurationType; description?: string; icon?: string; + isVisible: (driver: DBDriver) => boolean; } const driverConfiguration: IDriverConfiguration[] = [ { name: 'Manual', value: DriverConfigurationType.Manual, + isVisible: driver => driver.configurationTypes.includes(DriverConfigurationType.Manual), }, { name: 'URL', value: DriverConfigurationType.Url, + isVisible: driver => driver.configurationTypes.includes(DriverConfigurationType.Url), }, ]; -export const Options: TabContainerPanelComponent = observer(function Options({ state }) { +export const Options: TabContainerPanelComponent = observer(function Options({ formState, tabId }) { + const isAdmin = usePermission(EAdminPermission.admin); const serverConfigResource = useResource(Options, ServerConfigResource, undefined); - const connectionOptionsTabService = useService(ConnectionOptionsTabService); - const service = useService(ConnectionFormService); + const projectInfoResource = useService(ProjectInfoResource); const formRef = useRef(null); const translate = useTranslate(); - const { info, config, availableDrivers, submittingTask: submittingHandlers, disabled } = state; + const style = useS(styles); + const tabsState = useContext(TabsContext); + const isSharedProject = projectInfoResource.isProjectShared(formState.state.projectId); + const optionsPart = getConnectionFormOptionsPart(formState); + const connectionInfoAuthResource = useResource(Options, ConnectionInfoAuthPropertiesResource, optionsPart.connectionKey); + const connectionInfoOriginResource = useResource(Options, ConnectionInfoOriginResource, optionsPart.connectionKey); + const connectionInfoAuthPropertiesResource = useResource(Options, ConnectionInfoAuthPropertiesResource, optionsPart.connectionKey); + const configurationTypeLabel = translate('connections_connection_configuration'); //@TODO it's here until the profile implementation in the CloudBeaver - const readonly = state.readonly || info?.authModel === PROFILE_AUTH_MODEL_ID; + const readonly = formState.isDisabled || formState.isReadOnly || connectionInfoAuthResource.data?.authModel === PROFILE_AUTH_MODEL_ID; - const adminPermission = usePermission(EAdminPermission.admin); - - useFormValidator(submittingHandlers.for(service.formValidationTask), formRef.current); - const optionsHook = useOptions(state); + useFormValidator(formState.validationTask, formRef.current); const { credentialsSavingEnabled } = useAdministrationSettings(); - const handleAuthModelSelect = useCallback(async (value?: string, name?: string, prev?: string) => { - const model = applicableAuthModels.find(model => model?.id === value); + const driverMap = useResource(Options, DBDriverResource, { + key: optionsPart.state.driverId || null, + includes: ['includeProviderProperties', 'includeMainProperties', 'includeDriverProperties'] as const, + }); - if (!model) { - return; - } + const driver = driverMap.data; + const configurationTypes = driverConfiguration.filter(configuration => driver && configuration.isVisible(driver)); - optionsHook.setAuthModel(model); - }, []); + const applicableAuthModels = driver?.applicableAuthModels ?? []; - const driverMap = useResource( + const authModelLoader = useResource( Options, - DBDriverResource, - { key: config.driverId || null, includes: ['includeProviderProperties'] as const }, - { - onData: data => { - optionsHook.setDefaults(data); - }, - }, + DatabaseAuthModelsResource, + getComputed(() => optionsPart.state.authModelId || connectionInfoAuthResource.data?.authModel || driver?.defaultAuthModel || null), ); - const driver = driverMap.data; - const configurationTypes = driverConfiguration.filter(conf => driver?.configurationTypes.includes(conf.value)); + const authModel = authModelLoader.data; - function handleFormChange(value?: unknown, name?: string) { - if (name !== 'name' && optionsHook.isNameAutoFill()) { - optionsHook.updateNameTemplate(driver); - } + async function handleAuthModelSelect(authModelId: string | undefined) { + await optionsPart.setAuthModelId(authModelId); + } - if (config.template) { - config.folder = undefined; - } + const authentication = useAuthenticationAction({ + providerId: authModel?.requiredAuth ?? connectionInfoAuthResource.data?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID, + }); - if (name === 'sharedCredentials' && value) { - config.saveCredentials = true; + const edit = formState.mode === 'edit'; + const originLocal = + !connectionInfoAuthResource.data || (connectionInfoOriginResource.data?.origin && isLocalConnection(connectionInfoOriginResource.data.origin)); - for (const handler of config.networkHandlersConfig ?? []) { - if (!handler.savePassword) { - handler.savePassword = true; - } - } + const drivers = driverMap.resource.enabledDrivers.filter(({ id, driverInstalled }) => { + if (!edit && !isAdmin && !driverInstalled) { + return false; } - } - const { data: applicableAuthModels } = useResource( - Options, - DatabaseAuthModelsResource, - getComputed(() => (driver?.applicableAuthModels ? resourceKeyList(driver.applicableAuthModels) : CachedResourceListEmptyKey)), - ); + return formState.state.availableDrivers.includes(id); + }); - const { data: authModel } = useResource( - Options, - DatabaseAuthModelsResource, - getComputed(() => config.authModelId || info?.authModel || driver?.defaultAuthModel || null), - { - onData: data => optionsHook.setAuthModel(data), - }, - ); + function setProject(projectId: string) { + formState.state.projectId = projectId; + } - const authentication = useAuthenticationAction({ - providerId: authModel?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID, - }); + let properties = authModel?.properties; - const isURLConfiguration = config.configurationType === DriverConfigurationType.Url; - const edit = state.mode === 'edit'; - const originLocal = !info || isLocalConnection(info); + if ( + connectionInfoAuthPropertiesResource.data?.authProperties && + connectionInfoAuthPropertiesResource.data.authProperties.length > 0 && + optionsPart.state.authModelId === connectionInfoAuthResource.data?.authModel + ) { + properties = connectionInfoAuthPropertiesResource.data.authProperties; + } - const availableAuthModels = applicableAuthModels.filter(model => !!model && (adminPermission || !model.requiresLocalConfiguration)); - const drivers = driverMap.resource.enabledDrivers.filter(({ id }) => availableDrivers.includes(id)); + const sharedCredentials = optionsPart.state.sharedCredentials && serverConfigResource.data?.distributed; - let properties = authModel?.properties; + function openCredentialsTab(event: React.MouseEvent) { + event.preventDefault(); + tabsState?.open(CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID); + } - if (info?.authProperties && info.authProperties.length > 0 && config.authModelId === info.authModel) { - properties = info.authProperties; + async function setDriverIdHandler(driverId: string | undefined) { + await optionsPart.setDriverId(driverId); } - return styled(styles)( -
+ useAutoLoad(Options, optionsPart); + + return ( + - - - driver.id} - valueSelector={driver => driver.name ?? ''} - titleSelector={driver => driver.description} - iconSelector={driver => driver.icon} - searchable={drivers.length > 10} - readOnly={readonly || edit || drivers.length < 2} - disabled={disabled} - loading={driverMap.isLoading()} - tiny - fill - > - {translate('connections_connection_driver')} - - {configurationTypes.length > 1 && ( - <> - {/* conf.value} - valueSelector={conf => conf.name} - titleSelector={conf => conf.description} - readOnly={readonly || configurationTypes.length < 2} - disabled={disabled} + + {isAdmin && !driver?.driverInstalled && ( + + {translate('plugin_connections_connection_driver_not_installed_message')} + + )} + + + driver.id} + valueSelector={driver => driver.name ?? ''} + titleSelector={driver => driver.description} + iconSelector={driver => driver.icon} + readOnly={readonly || edit || drivers.length < 2} + disabled={formState.isDisabled} + loading={driverMap.isLoading()} tiny fill + onSelect={setDriverIdHandler} > - {translate('connections_connection_configuration')} - */} - + {translate('connections_connection_driver')} + + {configurationTypes.length > 1 && ( + - - {driverConfiguration.map(conf => ( - + + {configurationTypes.map(conf => ( + {conf.name} ))} - + )} + + {optionsPart.state.configurationType === DriverConfigurationType.Url && ( + + {translate('plugin_connections_connection_form_part_main_url_jdbc')} + )} - - {isURLConfiguration ? ( - - {translate('customConnection_url_JDBC')} - - ) : ( - - )} + + {optionsPart.state.configurationType === DriverConfigurationType.Manual && + (driver?.useCustomPage ? ( + + ) : ( + + ))} + - + {translate('connections_connection_name')} - {!config.template && ( - state.setProject(projectId)} - /> - )} - {!config.template && ( - - {translate('customConnection_folder')} - - )} + + + {translate('plugin_connections_connection_form_part_main_folder')} + - @@ -291,70 +281,72 @@ export const Options: TabContainerPanelComponent = observe {!driver?.anonymousAccess && (authentication.authorized || !edit) && ( {translate('connections_connection_edit_authentication')} - {availableAuthModels.length > 1 && ( - model!.id} - valueSelector={model => model!.displayName} - titleSelector={model => model?.description} - searchable={availableAuthModels.length > 10} - readOnly={readonly || !originLocal} - disabled={disabled} - tiny - fill - onSelect={handleAuthModelSelect} - /> + {serverConfigResource.resource.distributed && isSharedProject && ( + + {translate('connections_connection_share_credentials')} + )} - {authModel && properties && ( + + {!sharedCredentials ? ( <> - - - - {credentialsSavingEnabled && !config.template && ( - - - {translate('connections_connection_edit_save_credentials')} - - {serverConfigResource.resource.distributed && connectionOptionsTabService.isProjectShared(state) && ( - - {translate('connections_connection_share_credentials')} - - )} - )} + ) : ( + + {translate('plugin_connections_connection_form_shared_credentials_manage_info')} + + {translate('plugin_connections_connection_form_shared_credentials_manage_info_tab_link')} + + + )} + {!sharedCredentials && authModel && credentialsSavingEnabled && ( + + {translate( + !isSharedProject || serverConfigResource.data?.distributed + ? 'connections_connection_authentication_save_credentials_for_user' + : 'connections_connection_edit_save_credentials_shared', + )} + )} )} - {driver?.providerProperties && ( - - )} + {driver?.providerProperties && } + + -
, + ); }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ParametersForm.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ParametersForm.tsx index 09fdcc8b24..9fe2aac6e1 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ParametersForm.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ParametersForm.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -26,20 +26,20 @@ export const ParametersForm = observer(function ParametersForm({ config, {!embedded && ( - - {translate('customConnection_custom_host')} + + {translate('plugin_connections_connection_form_part_main_custom_host')} - - {translate('customConnection_custom_port')} + + {translate('plugin_connections_connection_form_part_main_custom_port')} )} - - {translate('customConnection_custom_database')} + + {translate('plugin_connections_connection_form_part_main_custom_database')} {requiresServerName && ( - - {translate('customConnection_custom_server_name')} + + {translate('plugin_connections_connection_form_part_main_custom_server_name')} )} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProviderPropertiesForm.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProviderPropertiesForm.tsx index 347a1c0705..cd97fbda2e 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProviderPropertiesForm.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProviderPropertiesForm.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,28 +10,34 @@ import { observer } from 'mobx-react-lite'; import { Container, Expandable, - getPropertyControlType, Group, GroupTitle, ObjectPropertyInfoForm, + Placeholder, useObjectPropertyCategories, useTranslate, } from '@cloudbeaver/core-blocks'; -import type { ConnectionConfig, DriverProviderPropertyInfoFragment } from '@cloudbeaver/core-sdk'; +import { type DriverPropertyInfoFragment, getObjectPropertyType } from '@cloudbeaver/core-sdk'; +import type { IFormState } from '@cloudbeaver/core-ui'; +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import { getConnectionFormOptionsPart } from './getConnectionFormOptionsPart.js'; +import { useService } from '@cloudbeaver/core-di'; +import { ConnectionFormService } from '../ConnectionFormService.js'; -type DriverProviderPropertyInfo = DriverProviderPropertyInfoFragment; +type DriverPropertyInfo = DriverPropertyInfoFragment; interface Props { - config: ConnectionConfig; - properties: DriverProviderPropertyInfo[]; - disabled?: boolean; + formState: IFormState; + properties: DriverPropertyInfo[]; readonly?: boolean; } -export const ProviderPropertiesForm = observer(function ProviderPropertiesForm({ config, properties, disabled, readonly }) { +export const ProviderPropertiesForm = observer(function ProviderPropertiesForm({ properties, readonly, formState }) { const translate = useTranslate(); - + const config = getConnectionFormOptionsPart(formState).state; + const disabled = formState.isDisabled; const supportedProperties = properties.filter(property => property.supportedConfigurationTypes?.some(type => type === config.configurationType)); + const connectionFormService = useService(ConnectionFormService); const { categories, isUncategorizedExists } = useObjectPropertyCategories(supportedProperties); @@ -44,6 +50,7 @@ export const ProviderPropertiesForm = observer(function ProviderPropertie return ( + {isUncategorizedExists && ( <> {translate('ui_settings')} @@ -84,7 +91,7 @@ export const ProviderPropertiesForm = observer(function ProviderPropertie category={category} disabled={disabled} readOnly={readonly} - geLayoutSize={property => (getPropertyControlType(property) === 'checkbox' ? { maximum: true } : { small: true, noGrow: true })} + geLayoutSize={property => (getObjectPropertyType(property) === 'checkbox' ? { maximum: true } : { small: true, noGrow: true })} hideEmptyPlaceholder />
@@ -95,6 +102,6 @@ export const ProviderPropertiesForm = observer(function ProviderPropertie ); }); -function isOnlyBooleans(properties: DriverProviderPropertyInfo[], category?: string): boolean { +function isOnlyBooleans(properties: DriverPropertyInfo[], category?: string): boolean { return properties.filter(property => !category || property.category === category).every(property => property.dataType === 'Boolean'); } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/getConnectionFormOptionsPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/getConnectionFormOptionsPart.ts new file mode 100644 index 0000000000..074964aa1b --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/getConnectionFormOptionsPart.ts @@ -0,0 +1,58 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import type { IFormState } from '@cloudbeaver/core-ui'; +import { ConnectionFormOptionsPart } from './ConnectionFormOptionsPart.js'; +import { + ConnectionInfoAuthPropertiesResource, + ConnectionInfoCustomOptionsResource, + ConnectionInfoProviderPropertiesResource, + ConnectionInfoResource, + DatabaseAuthModelsResource, + DBDriverExpertSettingsResource, + DBDriverResource, +} from '@cloudbeaver/core-connections'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; + +const DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART = createDataContext('Connection Form Options Part'); + +export function getConnectionFormOptionsPart(formState: IFormState): ConnectionFormOptionsPart { + return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const dbDriverResource = di.getService(DBDriverResource); + const projectInfoResource = di.getService(ProjectInfoResource); + const databaseAuthModelsResource = di.getService(DatabaseAuthModelsResource); + const connectionInfoResource = di.getService(ConnectionInfoResource); + const localizationService = di.getService(LocalizationService); + const commonDialogService = di.getService(CommonDialogService); + const notificationService = di.getService(NotificationService); + const connectionInfoAuthPropertiesResource = di.getService(ConnectionInfoAuthPropertiesResource); + const connectionInfoCustomOptionsResource = di.getService(ConnectionInfoCustomOptionsResource); + const connectionInfoProviderPropertiesResource = di.getService(ConnectionInfoProviderPropertiesResource); + const dbDriverExpertSettingsResource = di.getService(DBDriverExpertSettingsResource); + + return new ConnectionFormOptionsPart( + formState, + dbDriverResource, + projectInfoResource, + databaseAuthModelsResource, + connectionInfoResource, + connectionInfoAuthPropertiesResource, + connectionInfoCustomOptionsResource, + connectionInfoProviderPropertiesResource, + localizationService, + commonDialogService, + notificationService, + dbDriverExpertSettingsResource, + ); + }); +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/getConnectionName.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/getConnectionName.ts index 0f6567cd82..790c21cec3 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/getConnectionName.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/getConnectionName.ts @@ -1,13 +1,20 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ const MAX_HOST_LENGTH = 20; -export function getConnectionName(driverName: string, host?: string, port?: string, defaultPort?: string) { +interface IConnectionNameOptions { + driverName: string; + host?: string; + port?: string; + defaultPort?: string; +} + +export function getConnectionName({ driverName, host, port, defaultPort }: IConnectionNameOptions): string { let name = driverName; if (host) { diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/getDefaultConfigurationType.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/getDefaultConfigurationType.ts new file mode 100644 index 0000000000..b25fc1519f --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/getDefaultConfigurationType.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { DBDriver } from '@cloudbeaver/core-connections'; +import { DriverConfigurationType } from '@cloudbeaver/core-sdk'; + +export function getDefaultConfigurationType(driver: DBDriver | undefined) { + if (!driver) { + return DriverConfigurationType.Url; + } + + const supportManual = driver.configurationTypes.includes(DriverConfigurationType.Manual); + return supportManual ? DriverConfigurationType.Manual : DriverConfigurationType.Url; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/useOptions.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/useOptions.ts deleted file mode 100644 index 802a5c1f5a..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/useOptions.ts +++ /dev/null @@ -1,137 +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 { runInAction } from 'mobx'; - -import { useObjectRef } from '@cloudbeaver/core-blocks'; -import { DBDriver, DBDriverResource, isJDBCConnection } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { DatabaseAuthModel, DriverConfigurationType } from '@cloudbeaver/core-sdk'; - -import type { IConnectionFormState } from '../IConnectionFormProps'; -import { getConnectionName } from './getConnectionName'; - -export function useOptions(state: IConnectionFormState) { - const dbDriverResource = useService(DBDriverResource); - const refObject = useObjectRef( - () => ({ - prevName: null as string | null, - prevDriverId: null as string | null, - }), - { - state, - }, - ); - - return useObjectRef({ - updateNameTemplate(driver: DBDriver | undefined) { - runInAction(() => { - const { - state: { config, info }, - } = refObject; - - if (isJDBCConnection(driver, info)) { - refObject.prevName = config.url || ''; - config.name = config.url || ''; - return; - } - - if (!driver) { - config.name = 'New connection'; - return; - } - - const name = getConnectionName(driver.name || '', config.host, config.port, driver.defaultPort); - - refObject.prevName = name; - config.name = name; - }); - }, - setDefaults(driver: DBDriver | undefined) { - runInAction(() => { - const { - state: { config, info }, - prevDriverId, - } = refObject; - - if (info || driver?.id !== config.driverId) { - return; - } - - let prevDriver: DBDriver | undefined; - - if (prevDriverId) { - prevDriver = dbDriverResource.get(prevDriverId); - } - - refObject.prevDriverId = driver?.id || null; - - if (!config.configurationType || !driver?.configurationTypes.includes(config.configurationType)) { - config.configurationType = driver?.configurationTypes.includes(DriverConfigurationType.Manual) - ? DriverConfigurationType.Manual - : DriverConfigurationType.Url; - } - - if ((!prevDriver && config.host === undefined) || config.host === prevDriver?.defaultServer) { - config.host = driver?.defaultServer || 'localhost'; - } - - if ((!prevDriver && config.port === undefined) || config.port === prevDriver?.defaultPort) { - config.port = driver?.defaultPort; - } - - if ((!prevDriver && config.databaseName === undefined) || config.databaseName === prevDriver?.defaultDatabase) { - config.databaseName = driver?.defaultDatabase; - } - - if ((!prevDriver && config.url === undefined) || config.url === prevDriver?.sampleURL) { - config.url = driver?.sampleURL; - } - - if (this.isNameAutoFill()) { - this.updateNameTemplate(driver); - } - - if (driver?.id !== prevDriver?.id) { - config.credentials = {}; - config.providerProperties = {}; - config.authModelId = driver?.defaultAuthModel; - } - }); - }, - setAuthModel(model: DatabaseAuthModel) { - const { - state: { config, info }, - } = refObject; - - config.credentials = {}; - - if (model.id === info?.authModel) { - if (info.authProperties) { - for (const property of info.authProperties) { - if (!property.features.includes('password')) { - config.credentials[property.id!] = property.value; - } - } - } - } - - refObject.state.checkFormState(); - }, - - isNameAutoFill() { - const { - prevName, - state: { config, mode }, - } = refObject; - - const isAutoFill = config.name === prevName || prevName === null; - - return isAutoFill && mode === 'create'; - }, - }); -} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx index be88f5d3df..a62d5b6008 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,26 +8,38 @@ import { observer } from 'mobx-react-lite'; import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; -import { Button, getComputed, PlaceholderComponent, useResource, useTranslate } from '@cloudbeaver/core-blocks'; -import { DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections'; -import { useAuthenticationAction } from '@cloudbeaver/core-ui'; +import { Button, getComputed, type PlaceholderComponent, useResource, useTranslate, useAuthenticationAction } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoAuthPropertiesResource, DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections'; -import type { IConnectionFormProps } from '../IConnectionFormProps'; +import type { IConnectionFormProps } from '../IConnectionFormState.js'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; -export const AuthenticationButton: PlaceholderComponent = observer(function ConnectionFormAuthenticationAction({ state }) { +export const AuthenticationButton: PlaceholderComponent = observer(function ConnectionFormAuthenticationAction({ formState }) { const translate = useTranslate(); - const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, state.config.driverId || null); - + const optionsPart = getConnectionFormOptionsPart(formState); + const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, optionsPart.state.driverId || null); + const connectionInfoAuthPropertiesService = useResource( + ConnectionFormAuthenticationAction, + ConnectionInfoAuthPropertiesResource, + optionsPart.connectionKey, + ); + const info = connectionInfoAuthPropertiesService.data; const driver = driverMap.data; const { data: authModel } = useResource( ConnectionFormAuthenticationAction, DatabaseAuthModelsResource, - getComputed(() => state.config.authModelId || state.info?.authModel || driver?.defaultAuthModel || null), + getComputed(() => optionsPart.state.authModelId || info?.authModel || driver?.defaultAuthModel || null), ); const authentication = useAuthenticationAction({ - providerId: authModel?.requiredAuth ?? state.info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID, - onAuthenticate: () => state.loadConnectionInfo(), + providerId: authModel?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID, + onAuthenticate: () => { + if (!optionsPart.connectionKey) { + return; + } + + connectionInfoAuthPropertiesService.resource.markOutdated(optionsPart.connectionKey); + }, }); if (authentication.authorized) { @@ -35,27 +47,28 @@ export const AuthenticationButton: PlaceholderComponent = } return ( - ); }); export const ConnectionFormAuthenticationAction: PlaceholderComponent = observer(function ConnectionFormAuthenticationAction({ - state, + formState, }) { - const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, state.config.driverId || null); - + const optionsPart = getConnectionFormOptionsPart(formState); + const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, optionsPart.state.driverId || null); + const connectionInfoService = useResource(ConnectionFormAuthenticationAction, ConnectionInfoAuthPropertiesResource, optionsPart.connectionKey); const driver = driverMap.data; const { data: authModel } = useResource( ConnectionFormAuthenticationAction, DatabaseAuthModelsResource, - getComputed(() => state.config.authModelId || state.info?.authModel || driver?.defaultAuthModel || null), + getComputed(() => optionsPart.state.authModelId || connectionInfoService.data?.authModel || driver?.defaultAuthModel || null), ); - if (!authModel?.requiredAuth && !state.info?.requiredAuth) { + if (!authModel?.requiredAuth && !connectionInfoService.data?.requiredAuth) { return null; } - return ; + return ; }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormOriginInfoFormPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormOriginInfoFormPart.ts new file mode 100644 index 0000000000..064035bacc --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormOriginInfoFormPart.ts @@ -0,0 +1,165 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { FormPart, formStateContext, type IFormState } from '@cloudbeaver/core-ui'; + +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import type { IConnectionFormOriginInfoState } from './IConnectionFormOriginInfoState.js'; +import { + ConnectionInfoAuthPropertiesResource, + DatabaseAuthModelsResource, + DBDriverResource, + type ConnectionInfoOriginDetailsResource, +} from '@cloudbeaver/core-connections'; +import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, type UserInfoResource } from '@cloudbeaver/core-authentication'; +import { computed, makeObservable } from 'mobx'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import type { LocalizationService } from '@cloudbeaver/core-localization'; +import type { ConnectionFormOptionsPart } from '../Options/ConnectionFormOptionsPart.js'; + +const defaultStateGetter = () => ({}) as IConnectionFormOriginInfoState; + +export class ConnectionFormOriginInfoFormPart extends FormPart { + constructor( + formState: IFormState, + private readonly connectionInfoOriginDetailsResource: ConnectionInfoOriginDetailsResource, + private readonly userInfoResource: UserInfoResource, + private readonly databaseAuthModelsResource: DatabaseAuthModelsResource, + private readonly connectionInfoAuthPropertiesResource: ConnectionInfoAuthPropertiesResource, + private readonly dbDriverResource: DBDriverResource, + private readonly authProvidersResource: AuthProvidersResource, + private readonly localizationService: LocalizationService, + private readonly optionsPart: ConnectionFormOptionsPart, + ) { + super(formState, defaultStateGetter()); + + makeObservable(this, { + providerId: computed, + isAuthenticated: computed, + authModelId: computed, + }); + + this.formState.formStateTask.addHandler(this.formAuthState.bind(this)); + } + + override isOutdated(): boolean { + if (!this.optionsPart.connectionKey || !this.optionsPart.state.driverId) { + return false; + } + + if ( + this.dbDriverResource.isOutdated(this.optionsPart.state.driverId) || + this.connectionInfoAuthPropertiesResource.isOutdated(this.optionsPart.connectionKey) + ) { + return true; + } + + if (!this.authModelId) { + return false; + } + + if (this.databaseAuthModelsResource.isOutdated(this.authModelId) || this.userInfoResource.isOutdated()) { + return true; + } + + if (this.isAuthenticated && this.connectionInfoOriginDetailsResource.isOutdated(this.optionsPart.connectionKey)) { + return true; + } + + return false; + } + + get providerId(): string | null { + if (!this.formState.state.projectId || !this.optionsPart.state.driverId) { + return null; + } + + const driver = this.dbDriverResource.get(this.optionsPart.state.driverId); + + if (!driver) { + return null; + } + + const info = this.optionsPart.connectionKey ? this.connectionInfoAuthPropertiesResource.get(this.optionsPart.connectionKey) : null; + const authModel = this.databaseAuthModelsResource.get(this.optionsPart.state.authModelId ?? info?.authModel ?? driver?.defaultAuthModel ?? null); + + return authModel?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID; + } + + get isAuthenticated(): boolean { + if (!this.providerId) { + return false; + } + + return this.userInfoResource.hasToken(this.providerId); + } + + get authModelId(): string | null { + const driver = this.dbDriverResource.get(this.optionsPart.state.driverId!)!; + const info = this.optionsPart.connectionKey ? this.connectionInfoAuthPropertiesResource.get(this.optionsPart.connectionKey) : null; + + return this.optionsPart.state.authModelId ?? info?.authModel ?? driver?.defaultAuthModel ?? null; + } + + private async formAuthState(data: IConnectionFormState, contexts: IExecutionContextProvider) { + const stateContext = contexts.getContext(formStateContext); + + const info = this.optionsPart.connectionKey ? this.connectionInfoAuthPropertiesResource.get(this.optionsPart.connectionKey) : null; + const driver = await this.dbDriverResource.load(this.optionsPart.state.driverId!, ['includeProviderProperties', 'includeMainProperties']); + const [authModel] = await Promise.all([this.databaseAuthModelsResource.load(driver.defaultAuthModel), this.userInfoResource.load()]); + const providerId = authModel.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID; + + if (!this.userInfoResource.hasToken(providerId)) { + const provider = await this.authProvidersResource.load(providerId); + const message = this.localizationService.translate('plugin_connections_connection_cloud_auth_required', undefined, { + providerLabel: provider.label, + }); + stateContext.setInfo(message); + stateContext.readonly = this.formState.mode === 'edit'; + } + } + + protected override async loader(): Promise { + const state = defaultStateGetter(); + + if (!this.optionsPart.connectionKey || !this.optionsPart.state.driverId) { + this.setInitialState(state); + return; + } + + await Promise.all([ + this.dbDriverResource.load(this.optionsPart.state.driverId), + this.connectionInfoAuthPropertiesResource.load(this.optionsPart.connectionKey), + ]); + + if (!this.authModelId) { + throw new Error('Auth model is not defined'); + } + + await Promise.all([this.databaseAuthModelsResource.load(this.authModelId), this.userInfoResource.load()]); + + if (!this.isAuthenticated) { + this.setInitialState(state); + return; + } + + const originInfo = await this.connectionInfoOriginDetailsResource.load(this.optionsPart.connectionKey); + + if (!originInfo.origin.details) { + this.setInitialState(state); + return; + } + + for (const property of originInfo.origin.details) { + state[property.id!] = property.value; + } + + this.setInitialState(state); + } + + protected override async saveChanges(data: IFormState): Promise {} +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts index 113812c3d2..f92b1e8a24 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts @@ -1,59 +1,54 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 React from 'react'; -import { isLocalConnection } from '@cloudbeaver/core-connections'; +import { ConnectionInfoOriginResource, isLocalConnection } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; - -import { connectionFormConfigureContext } from '../connectionFormConfigureContext'; -import { ConnectionFormService } from '../ConnectionFormService'; -import type { IConnectionFormState } from '../IConnectionFormProps'; - -export const ConnectionFormAuthenticationAction = React.lazy(async () => { - const { ConnectionFormAuthenticationAction } = await import('./ConnectionFormAuthenticationAction'); - return { default: ConnectionFormAuthenticationAction }; -}); -export const OriginInfo = React.lazy(async () => { - const { OriginInfo } = await import('./OriginInfo'); - return { default: OriginInfo }; -}); -export const OriginInfoTab = React.lazy(async () => { - const { OriginInfoTab } = await import('./OriginInfoTab'); - return { default: OriginInfoTab }; -}); - -@injectable() + +import { ConnectionFormService } from '../ConnectionFormService.js'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; +export const ConnectionFormAuthenticationAction = importLazyComponent(() => + import('./ConnectionFormAuthenticationAction.js').then(m => m.ConnectionFormAuthenticationAction), +); + +const OriginInfo = importLazyComponent(() => import('./OriginInfo.js').then(m => m.OriginInfo)); +const OriginInfoTab = importLazyComponent(() => import('./OriginInfoTab.js').then(m => m.OriginInfoTab)); + +@injectable(() => [ConnectionFormService, ConnectionInfoOriginResource]) export class ConnectionOriginInfoTabService extends Bootstrap { - constructor(private readonly connectionFormService: ConnectionFormService) { + constructor( + private readonly connectionFormService: ConnectionFormService, + private readonly connectionInfoOriginResource: ConnectionInfoOriginResource, + ) { super(); } - register(): void { - this.connectionFormService.tabsContainer.add({ + override register(): void { + this.connectionFormService.parts.add({ key: 'origin', order: 3, tab: () => OriginInfoTab, panel: () => OriginInfo, - stateGetter: () => () => ({}), - isHidden: (tabId, props) => (props?.state.info ? isLocalConnection(props.state.info) : true), + getLoader: () => getCachedMapResourceLoaderState(this.connectionInfoOriginResource, () => CachedMapAllKey), + isHidden: (tabId, props) => { + const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null; + const key = optionsPart?.connectionKey; + const originInfo = key ? this.connectionInfoOriginResource.get(key) : null; + + if (!originInfo) { + return true; + } + + return isLocalConnection(originInfo.origin); + }, }); - this.connectionFormService.configureTask.addHandler(this.configure.bind(this)); - this.connectionFormService.actionsContainer.add(ConnectionFormAuthenticationAction, 0); } - - load(): void {} - - private configure(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const configuration = contexts.getContext(connectionFormConfigureContext); - - configuration.include('includeOrigin'); - } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/IConnectionFormOriginInfoState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/IConnectionFormOriginInfoState.ts new file mode 100644 index 0000000000..da5bfadc43 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/IConnectionFormOriginInfoState.ts @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { schema } from '@cloudbeaver/core-utils'; + +const CONNECTION_FORM_ORIGIN_INFO_SCHEMA = schema.record( + schema.string(), + schema.object({ + id: schema.string().optional(), + required: schema.boolean(), + displayName: schema.string().optional(), + description: schema.string().optional(), + category: schema.string().optional(), + dataType: schema.string().optional(), + defaultValue: schema.any().optional(), + validValues: schema.array(schema.any()).optional(), + value: schema.any().optional(), + length: schema.string(), + features: schema.array(schema.string()), + order: schema.number(), + }), +); + +export type IConnectionFormOriginInfoState = schema.infer; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.module.css b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.module.css new file mode 100644 index 0000000000..15df602273 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.module.css @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.loader { + height: 100%; +} + +.coloredContainer { + flex: 1; + overflow: auto; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx index a073ceecbc..50811db52c 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx @@ -1,118 +1,103 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { runInAction } from 'mobx'; import { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; +import { AuthProvidersResource } from '@cloudbeaver/core-authentication'; import { ColoredContainer, ExceptionMessage, + getComputed, Group, Loader, ObjectPropertyInfoForm, + s, TextPlaceholder, + useAutoLoad, useResource, - useStyles, + useS, useTranslate, } from '@cloudbeaver/core-blocks'; -import { createConnectionParam } from '@cloudbeaver/core-connections'; -import { TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; +import { ConnectionInfoOriginDetailsResource, ConnectionInfoResource } from '@cloudbeaver/core-connections'; +import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '../IConnectionFormProps'; +import styles from './OriginInfo.module.css'; +import type { IConnectionFormProps } from '../IConnectionFormState.js'; -const style = css` - Loader { - height: 100%; - } - ColoredContainer { - flex: 1; - overflow: auto; - } - ExceptionMessage { - padding: 16px; - } -`; +import { getConnectionFormOriginInfoFormPart } from './getConnectionFormOriginInfoFormPart.js'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; -export const OriginInfo: TabContainerPanelComponent = observer(function OriginInfo({ tabId, state: { info, resource } }) { +export const OriginInfo: TabContainerPanelComponent = observer(function OriginInfo({ tabId, formState }) { const tab = useTab(tabId); const translate = useTranslate(); - // const userInfoService = useService(UserInfoResource); - const state = useTabState>(); - const styles = useStyles(style); + const originInfoPart = getConnectionFormOriginInfoFormPart(formState); + const style = useS(styles); + const optionsPart = getConnectionFormOptionsPart(formState); + const providerLoader = useResource(OriginInfo, AuthProvidersResource, originInfoPart.providerId, { + active: tab.selected, + }); + const isAuthenticated = getComputed(() => originInfoPart.isAuthenticated); + const connectionOriginDetailsResource = useResource(OriginInfo, ConnectionInfoOriginDetailsResource, optionsPart.connectionKey, { + active: tab.selected && isAuthenticated, + }); + const connection = useResource(OriginInfo, ConnectionInfoResource, optionsPart.connectionKey, { + active: tab.selected && isAuthenticated, + }); - const connection = useResource( - OriginInfo, - resource, - { - key: tab.selected && info ? createConnectionParam(info.projectId, info.id) : null, - includes: ['includeOrigin', 'customIncludeOriginDetails'] as const, - }, - { - // isActive: () => !info?.origin || userInfoService.hasOrigin(info.origin), - onData: connection => { - runInAction(() => { - if (!connection.origin.details) { - return; - } - - for (const property of Object.keys(state)) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete state[property]; - } - - for (const property of connection.origin.details) { - state[property.id!] = property.value; - } - }); - }, - }, - ); + useAutoLoad(OriginInfo, originInfoPart); if (connection.isLoading()) { - return styled(styles)( - - - , + return ( + + + ); } if (connection.exception) { - return styled(styles)( - + return ( + - , + ); } - // const authorized = !info?.origin || userInfoService.hasOrigin(info.origin); - - // if (!authorized && info?.origin) { - // return styled(styles)( - // - // - // - // ); - // } + if (!isAuthenticated) { + return ( + + + {translate('plugin_connections_connection_cloud_auth_required', undefined, { + providerLabel: providerLoader.data?.label, + })} + + + ); + } - if (!connection.data?.origin.details || connection.data.origin.details.length === 0) { - return styled(styles)( - - {translate('connections_administration_connection_no_information')} - , + if (!connectionOriginDetailsResource.data?.origin.details || connectionOriginDetailsResource.data?.origin.details.length === 0) { + return ( + + {translate('core_connections_connection_no_information')} + ); } - return styled(styles)( - + return ( + - + - - , + + ); }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx index 39b665866a..5c14e49f38 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx @@ -1,24 +1,27 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled from 'reshadow'; -import { Translate, useStyles } from '@cloudbeaver/core-blocks'; -import { Tab, TabContainerTabComponent, TabTitle } from '@cloudbeaver/core-ui'; +import { Translate, useResource } from '@cloudbeaver/core-blocks'; +import { Tab, type TabContainerTabComponent, TabTitle } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '../IConnectionFormProps'; +import type { IConnectionFormProps } from '../IConnectionFormState.js'; +import { ConnectionInfoOriginResource } from '@cloudbeaver/core-connections'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; +export const OriginInfoTab: TabContainerTabComponent = observer(function OriginInfoTab({ formState, ...rest }) { + const optionsPart = getConnectionFormOptionsPart(formState); + const connectionInfoOriginResource = useResource(OriginInfoTab, ConnectionInfoOriginResource, optionsPart.connectionKey); -export const OriginInfoTab: TabContainerTabComponent = observer(function OriginInfoTab({ state: { info }, style, ...rest }) { - return styled(useStyles(style))( - + return ( + - + - , + ); }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/getConnectionFormOriginInfoFormPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/getConnectionFormOriginInfoFormPart.ts new file mode 100644 index 0000000000..678cdf9eb1 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/getConnectionFormOriginInfoFormPart.ts @@ -0,0 +1,51 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 IFormState } from '@cloudbeaver/core-ui'; + +import { + ConnectionInfoAuthPropertiesResource, + ConnectionInfoOriginDetailsResource, + DatabaseAuthModelsResource, + DBDriverResource, +} from '@cloudbeaver/core-connections'; +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import { ConnectionFormOriginInfoFormPart } from './ConnectionFormOriginInfoFormPart.js'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import { AuthProvidersResource, UserInfoResource } from '@cloudbeaver/core-authentication'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; + +const DATA_CONTEXT_CONNECTION_FORM_ORIGIN_INFO_FORM_PART = createDataContext( + 'Connection Form Origin Info Form Part', +); + +export function getConnectionFormOriginInfoFormPart(formState: IFormState): ConnectionFormOriginInfoFormPart { + return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_ORIGIN_INFO_FORM_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const connectionInfoOriginDetailsResource = di.getService(ConnectionInfoOriginDetailsResource); + const userInfoResource = di.getService(UserInfoResource); + const databaseAuthModelsResource = di.getService(DatabaseAuthModelsResource); + const connectionInfoAuthPropertiesResource = di.getService(ConnectionInfoAuthPropertiesResource); + const dbDriverResource = di.getService(DBDriverResource); + const authProvidersResource = di.getService(AuthProvidersResource); + const localizationService = di.getService(LocalizationService); + const optionsPart = getConnectionFormOptionsPart(formState); + + return new ConnectionFormOriginInfoFormPart( + formState, + connectionInfoOriginDetailsResource, + userInfoResource, + databaseAuthModelsResource, + connectionInfoAuthPropertiesResource, + dbDriverResource, + authProvidersResource, + localizationService, + optionsPart, + ); + }); +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionFormSSHPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionFormSSHPart.ts new file mode 100644 index 0000000000..3fe10a0d63 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionFormSSHPart.ts @@ -0,0 +1,207 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui'; + +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { + DriverConfigurationType, + NetworkHandlerAuthType, + type NetworkHandlerConfigInput, + type NetworkHandlerDescriptor, +} from '@cloudbeaver/core-sdk'; +import { ConnectionInfoNetworkHandlersResource, NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; +import { toJS } from 'mobx'; +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import type { INetworkHandlerConfig } from '../Options/IConnectionNetworkHanler.js'; +import { ConnectionFormOptionsPart } from '../Options/ConnectionFormOptionsPart.js'; + +const getDefaultState = () => + ({ + id: SSH_TUNNEL_ID, + enabled: false, + authType: NetworkHandlerAuthType.Password, + // should initially undefined cause if it's empty string it counts as saved password + password: undefined, + savePassword: false, + userName: '', + // should initially undefined cause if it's empty string it counts as saved private key + key: undefined, + properties: { + port: 22, + host: '', + aliveInterval: '0', + sshConnectTimeout: '10000', + }, + }) as INetworkHandlerConfig; + +export class ConnectionFormSSHPart extends FormPart { + constructor( + formState: IFormState, + private readonly networkHandlerResource: NetworkHandlerResource, + private readonly connectionInfoNetworkHandlersResource: ConnectionInfoNetworkHandlersResource, + private readonly optionsPart: ConnectionFormOptionsPart, + ) { + super(formState, getDefaultState()); + } + + override isOutdated(): boolean { + if (this.networkHandlerResource.isOutdated(SSH_TUNNEL_ID)) { + return true; + } + if (!this.optionsPart.connectionKey) { + return false; + } + + return this.connectionInfoNetworkHandlersResource.isOutdated(this.optionsPart.connectionKey); + } + + protected override async loader(): Promise { + const handler = await this.networkHandlerResource.load(SSH_TUNNEL_ID); + if (!this.optionsPart.connectionKey) { + const state = getDefaultState(); + this.copyInitialHandlerProperties(handler, state); + this.setInitialState(state); + return; + } + + const connection = await this.connectionInfoNetworkHandlersResource.load(this.optionsPart.connectionKey); + const sshHandler = connection?.networkHandlersConfig?.find(h => h.id === SSH_TUNNEL_ID); + + const state = toJS(sshHandler ?? getDefaultState()); + this.copyInitialHandlerProperties(handler, state); + this.setInitialState(state); + } + + private copyInitialHandlerProperties(handlerDescriptor: NetworkHandlerDescriptor, state: INetworkHandlerConfig): void { + const properties: Record = {}; + if (handlerDescriptor) { + for (const property of handlerDescriptor.properties) { + if (!property.features.includes('password')) { + properties[property.id!] = property.value; + } + } + } + + state.properties = { + ...properties, + ...state.properties, + }; + } + + protected override async saveChanges( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise {} + + protected override format( + data: IFormState, + contexts: IExecutionContextProvider>, + ): void | Promise { + const urlType = this.optionsPart.state.configurationType === DriverConfigurationType.Url; + + if (urlType) { + return; + } + + const passwordChanged = isPasswordChanged(this.state, this.initialState); + const keyChanged = isKeyChanged(this.state, this.initialState); + + let handlerConfig: NetworkHandlerConfigInput = { + ...this.state, + savePassword: this.state.savePassword || this.optionsPart.state.sharedCredentials, + key: this.state.authType === NetworkHandlerAuthType.PublicKey && keyChanged ? this.state.key : undefined, + password: passwordChanged ? this.state.password : undefined, + }; + + delete handlerConfig.secureProperties; + + if (this.state.enabled && !this.state.savePassword) { + this.formState.state.requiredNetworkHandlersIds.push(this.state.id); + } else if (!this.state.enabled) { + this.formState.state.requiredNetworkHandlersIds = this.formState.state.requiredNetworkHandlersIds.filter(id => id !== this.state.id); + } + + if (handlerConfig) { + handlerConfig = getTrimmedSSHConfig(handlerConfig); + this.optionsPart.state.networkHandlersConfig!.push(handlerConfig); + } + } + + protected override validate( + data: IFormState, + contexts: IExecutionContextProvider>, + ): void | Promise { + const validation = contexts.getContext(formValidationContext); + + if (!this.isChanged || !this.state.enabled) { + return; + } + + if (this.state.savePassword && !this.state.userName?.length) { + validation.error("Field SSH 'User' can't be empty"); + } + + if (!this.state.properties?.['host']?.length) { + validation.error("Field SSH 'Host' can't be empty"); + } + + const port = Number(this.state.properties?.['port']); + if (Number.isNaN(port) || port < 1) { + validation.error("Field SSH 'Port' can't be empty"); + } + + const keyAuth = this.state.authType === NetworkHandlerAuthType.PublicKey; + const keySaved = this.initialState?.key === ''; + if (keyAuth && this.state.savePassword && !keySaved && !this.state.key?.length) { + validation.error("Field SSH 'Private key' can't be empty"); + } + + const passwordSaved = this.initialState?.password === '' && this.initialState?.authType === this.state.authType; + + if (!keyAuth && this.state.savePassword && !passwordSaved && !this.state.password?.length) { + validation.error("Field SSH 'Password' can't be empty"); + } + } +} + +function getTrimmedSSHConfig(input: NetworkHandlerConfigInput): NetworkHandlerConfigInput { + const trimmedInput = toJS(input); + const attributesToTrim = Object.keys(input) as (keyof NetworkHandlerConfigInput)[]; + + for (const key of attributesToTrim) { + if (typeof trimmedInput[key] === 'string') { + trimmedInput[key] = trimmedInput[key]?.trim(); + } + } + + for (const key in trimmedInput.properties) { + if (typeof trimmedInput.properties[key] === 'string') { + trimmedInput.properties[key] = trimmedInput.properties[key]?.trim(); + } + } + + return trimmedInput; +} + +function isPasswordChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) { + if (!initial && !handler.enabled) { + return false; + } + + return ( + (((initial?.password === null && handler.password !== null) || initial?.password === '') && handler.password !== '') || !!handler.password?.length + ); +} + +function isKeyChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) { + if (!initial && !handler.enabled) { + return false; + } + + return (((initial?.key === null && handler.key !== null) || initial?.key === '') && handler.key !== '') || !!handler.key?.length; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts index 055ea36ba5..ce8b936a36 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts @@ -1,56 +1,49 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, makeObservable } from 'mobx'; -import React from 'react'; import { DBDriverResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { DriverConfigurationType, NetworkHandlerAuthType, NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; -import { formStateContext } from '@cloudbeaver/core-ui'; +import { DriverConfigurationType } from '@cloudbeaver/core-sdk'; -import { connectionFormConfigureContext } from '../connectionFormConfigureContext'; -import { ConnectionFormService } from '../ConnectionFormService'; -import { connectionConfigContext } from '../Contexts/connectionConfigContext'; -import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext'; -import type { IConnectionFormFillConfigData, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps'; +import { ConnectionFormService } from '../ConnectionFormService.js'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; +import { getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; -export const SSHTab = React.lazy(async () => { - const { SSHTab } = await import('./SSHTab'); - return { default: SSHTab }; -}); -export const SSHPanel = React.lazy(async () => { - const { SSHPanel } = await import('./SSHPanel'); - return { default: SSHPanel }; -}); +const SSHTab = importLazyComponent(() => import('./SSHTab.js').then(m => m.SSHTab)); +const SSHPanel = importLazyComponent(() => import('./SSHPanel.js').then(m => m.SSHPanel)); -@injectable() +@injectable(() => [DBDriverResource, ConnectionFormService]) export class ConnectionSSHTabService extends Bootstrap { - constructor(private readonly connectionFormService: ConnectionFormService, private readonly dbDriverResource: DBDriverResource) { + constructor( + private readonly dbDriverResource: DBDriverResource, + private readonly connectionFormService: ConnectionFormService, + ) { super(); - - makeObservable(this, { - fillConfig: action, - prepareConfig: action, - }); } - register(): void { - this.connectionFormService.tabsContainer.add({ + override register(): void { + this.connectionFormService.parts.add({ key: 'ssh', - name: 'customConnection_options', - order: 3, + name: 'plugin_connections_connection_form_part_main', + order: 2.5, tab: () => SSHTab, panel: () => SSHPanel, + getLoader: (_, props) => { + const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null; + return [getCachedMapResourceLoaderState(this.dbDriverResource, () => optionsPart?.state.driverId ?? null)]; + }, isHidden: (tabId, props) => { - if (props?.state.config.driverId) { - const driver = this.dbDriverResource.get(props.state.config.driverId); - const urlType = props.state.config.configurationType === DriverConfigurationType.Url; + const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null; + + if (optionsPart?.state.driverId) { + const driver = this.dbDriverResource.get(optionsPart.state.driverId); + const urlType = optionsPart.state.configurationType === DriverConfigurationType.Url; return urlType || !driver?.applicableNetworkHandlers.includes(SSH_TUNNEL_ID); } @@ -58,193 +51,5 @@ export class ConnectionSSHTabService extends Bootstrap { return true; }, }); - - this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this)); - - this.connectionFormService.formValidationTask.addHandler(this.validate.bind(this)); - - this.connectionFormService.formStateTask.addHandler(this.formState.bind(this)); - - this.connectionFormService.configureTask.addHandler(this.configure.bind(this)); - - this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this)); - } - - load(): void {} - - private fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider) { - if (!updated) { - return; - } - const initialConfig = state.info?.networkHandlersConfig?.find(handler => handler.id === SSH_TUNNEL_ID); - - if (!state.config.networkHandlersConfig) { - state.config.networkHandlersConfig = []; - } - - if (!state.config.networkHandlersConfig.some(state => state.id === SSH_TUNNEL_ID)) { - state.config.networkHandlersConfig.push({ - id: SSH_TUNNEL_ID, - enabled: false, - authType: NetworkHandlerAuthType.Password, - password: '', - savePassword: false, - userName: '', - key: '', - ...initialConfig, - properties: { - port: 22, - host: '', - aliveInterval: '0', - sshConnectTimeout: '10000', - ...initialConfig?.properties, - }, - }); - } - } - - private configure(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const configuration = contexts.getContext(connectionFormConfigureContext); - - configuration.include('includeNetworkHandlersConfig'); - } - - private validate({ state: { config, info } }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const validation = contexts.getContext(this.connectionFormService.connectionValidationContext); - - if (!config.networkHandlersConfig) { - return; - } - - const handler = config.networkHandlersConfig.find(handler => handler.id === SSH_TUNNEL_ID); - - if (!handler) { - return; - } - - if (handler.enabled) { - const initial = info?.networkHandlersConfig?.find(h => h.id === handler.id); - if (this.isChanged(handler, initial)) { - if (handler.savePassword && !handler.userName?.length) { - validation.error("Field SSH 'User' can't be empty"); - } - - if (!handler.properties?.host?.length) { - validation.error("Field SSH 'Host' can't be empty"); - } - - const port = Number(handler.properties?.port); - if (Number.isNaN(port) || port < 1) { - validation.error("Field SSH 'Port' can't be empty"); - } - } - - const keyAuth = handler.authType === NetworkHandlerAuthType.PublicKey; - const keySaved = initial?.key === ''; - if (keyAuth && handler.savePassword && !keySaved && !handler.key?.length) { - validation.error("Field SSH 'Private key' can't be empty"); - } - - const passwordSaved = initial?.password === '' && initial.authType === handler.authType; - if (!keyAuth && handler.savePassword && !passwordSaved && !handler.password?.length) { - validation.error("Field SSH 'Password' can't be empty"); - } - } - } - - private prepareConfig({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - const credentialsState = contexts.getContext(connectionCredentialsStateContext); - const urlType = state.config.configurationType === DriverConfigurationType.Url; - - if (urlType || !state.config.networkHandlersConfig || state.config.networkHandlersConfig.length === 0) { - return; - } - - let handlerConfig: NetworkHandlerConfigInput | undefined; - - const handler = state.config.networkHandlersConfig.find(handler => handler.id === SSH_TUNNEL_ID); - - if (!handler) { - return; - } - - const initial = state.info?.networkHandlersConfig?.find(h => h.id === handler.id); - const passwordChanged = this.isPasswordChanged(handler, initial); - const keyChanged = this.isKeyChanged(handler, initial); - - if (this.isChanged(handler, initial) || passwordChanged || keyChanged) { - handlerConfig = { - ...handler, - key: handler.authType === NetworkHandlerAuthType.PublicKey && keyChanged ? handler.key : undefined, - password: passwordChanged ? handler.password : undefined, - }; - - delete handlerConfig.secureProperties; - } - - if (handler.enabled && !handler.savePassword) { - credentialsState.requireNetworkHandler(handler.id); - } - - if (handlerConfig) { - if (!config.networkHandlersConfig) { - config.networkHandlersConfig = []; - } - - config.networkHandlersConfig.push(handlerConfig); - } - } - - private formState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - if (config.networkHandlersConfig !== undefined) { - const stateContext = contexts.getContext(formStateContext); - - stateContext.markEdited(); - } - } - - private isChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) { - if (!initial && !handler.enabled) { - return false; - } - - const port = Number(initial?.properties?.port); - const formPort = Number(handler.properties?.port); - - if ( - handler.enabled !== initial?.enabled || - handler.authType !== initial?.authType || - handler.savePassword !== initial?.savePassword || - handler.userName !== initial?.userName || - handler.properties?.host !== initial?.properties?.host || - port !== formPort || - handler.properties?.aliveInterval !== initial?.properties?.aliveInterval || - handler.properties?.sshConnectTimeout !== initial?.properties?.sshConnectTimeout - ) { - return true; - } - - return false; - } - - private isPasswordChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) { - if (!initial && !handler.enabled) { - return false; - } - - return ( - (((initial?.password === null && handler.password !== null) || initial?.password === '') && handler.password !== '') || - !!handler.password?.length - ); - } - - private isKeyChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) { - if (!initial && !handler.enabled) { - return false; - } - - return (((initial?.key === null && handler.key !== null) || initial?.key === '') && handler.key !== '') || !!handler.key?.length; } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/IConnectionFromSSHState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/IConnectionFromSSHState.ts new file mode 100644 index 0000000000..4ffdd6b7cc --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/IConnectionFromSSHState.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { schema } from '@cloudbeaver/core-utils'; +import { CONNECTION_NETWORK_HANDLER_SCHEMA } from '../Options/IConnectionNetworkHanler.js'; + +export const CONNECTION_FORM_SSH_SCHEMA = schema.object({}).extend(CONNECTION_NETWORK_HANDLER_SCHEMA.shape); + +export type IConnectionFromSSHState = schema.infer; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.module.css b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.module.css new file mode 100644 index 0000000000..baa382f6c2 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.module.css @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.form { + display: flex; + flex-direction: column; + flex: 1; + overflow: auto; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx index 6bfb43895f..60a8241e76 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx @@ -1,18 +1,17 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useCallback, useState } from 'react'; -import styled, { css } from 'reshadow'; import { Button, ColoredContainer, - Combobox, + Select, Container, Expandable, FieldCheckbox, @@ -20,69 +19,63 @@ import { Group, GroupItem, InputField, + s, Switch, useAdministrationSettings, + useAutoLoad, useResource, - useStyles, + useS, useTranslate, } from '@cloudbeaver/core-blocks'; import { NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; -import { NetworkHandlerAuthType, NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; -import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; +import { useService } from '@cloudbeaver/core-di'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { NetworkHandlerAuthType, type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; +import { useTab, type IFormState, type TabContainerPanelComponent } from '@cloudbeaver/core-ui'; import { isSafari } from '@cloudbeaver/core-utils'; -import type { IConnectionFormProps } from '../IConnectionFormProps'; -import { authTypes } from './authTypes'; -import { SSHKeyUploader } from './SSHKeyUploader'; +import { authTypes } from './authTypes.js'; +import styles from './SSH.module.css'; +import { SSHKeyUploader } from './SSHKeyUploader.js'; +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import { getConnectionFormSSHPart } from './getConnectionFormSSHPart.js'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; -const SSH_STYLES = css` - Form { - display: flex; - flex-direction: column; - flex: 1; - overflow: auto; - } -`; - -interface Props extends IConnectionFormProps { +interface Props { handlerState: NetworkHandlerConfigInput; + formState: IFormState; } -export const SSH: TabContainerPanelComponent = observer(function SSH({ state: formState, handlerState }) { - const { info, readonly, disabled: formDisabled } = formState; +export const SSH: TabContainerPanelComponent = observer(function SSH({ formState, handlerState, tabId }) { + const { selected } = useTab(tabId); const [loading, setLoading] = useState(false); const { credentialsSavingEnabled } = useAdministrationSettings(); - - const initialConfig = info?.networkHandlersConfig?.find(handler => handler.id === SSH_TUNNEL_ID); - - const resource = useResource(SSH, NetworkHandlerResource, SSH_TUNNEL_ID, { - onData: handler => { - if (Object.keys(handlerState).length === 0) { - for (const property of handler.properties) { - if (!property.features.includes('password')) { - handlerState.properties[property.id!] = property.value; - } - } - } - }, + const networkHandlerResource = useService(NetworkHandlerResource); + const serverConfigResource = useResource(SSH, ServerConfigResource, undefined, { + active: selected, }); - const testConnection = async () => { + async function testConnection() { setLoading(true); - await resource.resource.test(handlerState); + await networkHandlerResource.test(handlerState); setLoading(false); - }; + } - const styles = useStyles(SSH_STYLES); + const SSHPart = getConnectionFormSSHPart(formState); + const style = useS(styles); const translate = useTranslate(); - const disabled = formDisabled || loading; + const disabled = formState.isDisabled || loading || formState.isReadOnly; const enabled = handlerState.enabled || false; const keyAuth = handlerState.authType === NetworkHandlerAuthType.PublicKey; - const passwordFilled = (initialConfig?.password === null && handlerState.password !== '') || !!handlerState.password?.length; + const passwordFilled = (SSHPart.initialState?.password === null && handlerState.password !== '') || !!handlerState.password?.length; const testAvailable = keyAuth ? !!handlerState.key?.length : passwordFilled; const passwordLabel = keyAuth ? 'Passphrase' : translate('connections_network_handler_ssh_tunnel_password'); - const passwordSaved = initialConfig?.password === '' && initialConfig.authType === handlerState.authType; - const keySaved = initialConfig?.key === ''; + const passwordSaved = SSHPart.initialState?.password === '' && SSHPart.initialState.authType === handlerState.authType; + const keySaved = SSHPart.initialState?.key === ''; + const projectInfoResource = useService(ProjectInfoResource); + const isSharedProject = projectInfoResource.isProjectShared(formState.state.projectId); + const optionsPart = getConnectionFormOptionsPart(formState); const aliveIntervalLabel = translate('connections_network_handler_ssh_tunnel_advanced_settings_alive_interval'); const connectTimeoutLabel = translate('connections_network_handler_ssh_tunnel_advanced_settings_connect_timeout'); @@ -91,48 +84,32 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st handlerState.password = ''; }, []); - return styled(styles)( -
+ useAutoLoad(SSH, [SSHPart, optionsPart], selected); + + return ( + - + {translate('connections_network_handler_ssh_tunnel_enable')} - value.key} valueSelector={value => value.label} - disabled={disabled || readonly || !enabled} + disabled={disabled || !enabled} tiny onSelect={authTypeChangeHandler} > {translate('connections_network_handler_ssh_tunnel_auth_type')} - + - + {translate('connections_network_handler_ssh_tunnel_host')} - + {translate('connections_network_handler_ssh_tunnel_port')} @@ -141,9 +118,7 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st type="text" name="userName" state={handlerState} - disabled={disabled || !enabled} - readOnly={readonly} - mod="surface" + readOnly={disabled || !enabled} required={handlerState.savePassword} tiny fill @@ -155,26 +130,33 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st name="password" autoComplete={isSafari ? 'section-connection-ssh-authentication section-ssh password' : 'new-password'} state={handlerState} - disabled={disabled || !enabled} - readOnly={readonly} - mod="surface" - required={!keyAuth && handlerState.savePassword} + readOnly={disabled || !enabled} + required={!passwordSaved && !keyAuth && handlerState.savePassword} description={passwordSaved ? translate('ui_processing_saved') : undefined} tiny fill > {passwordLabel} - {keyAuth && } + {keyAuth && } - {credentialsSavingEnabled && !formState.config.template && ( + {credentialsSavingEnabled && !optionsPart.state.sharedCredentials && ( - {translate('connections_connection_edit_save_credentials')} + {translate( + !isSharedProject || serverConfigResource.data?.distributed + ? 'connections_connection_authentication_save_credentials_for_user' + : 'connections_connection_edit_save_credentials_shared', + )} )} @@ -184,10 +166,8 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st type="number" name="aliveInterval" state={handlerState.properties} - disabled={disabled || !enabled} - readOnly={readonly} + readOnly={disabled || !enabled} labelTooltip={aliveIntervalLabel} - mod="surface" tiny > {aliveIntervalLabel} @@ -196,10 +176,8 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st type="number" name="sshConnectTimeout" state={handlerState.properties} - disabled={disabled || !enabled} - readOnly={readonly} + readOnly={disabled || !enabled} labelTooltip={connectTimeoutLabel} - mod="surface" tiny > {connectTimeoutLabel} @@ -208,12 +186,12 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st - -
, + ); }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHKeyUploader.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHKeyUploader.tsx index 4ff3e99ed6..f5669f319e 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHKeyUploader.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHKeyUploader.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -45,14 +45,14 @@ export const SSHKeyUploader = observer(function SSHKeyUploader({ state, s disabled={disabled} readOnly={readonly} description={saved ? translate('ui_processing_saved') : undefined} - required={state.savePassword} + required={state.savePassword && !saved} medium > {translate('connections_network_handler_ssh_tunnel_private_key')} - diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHPanel.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHPanel.tsx index 28de5386a6..fddcacb926 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHPanel.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHPanel.tsx @@ -1,25 +1,27 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; -import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; +import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '../IConnectionFormProps'; -import { SSH } from './SSH'; +import { SSH } from './SSH.js'; +import { useAutoLoad } from '@cloudbeaver/core-blocks'; +import { getConnectionFormSSHPart } from './getConnectionFormSSHPart.js'; +import type { IConnectionFormProps } from '../IConnectionFormState.js'; export const SSHPanel: TabContainerPanelComponent = observer(function SSHPanel(props) { - const state = props.state.config.networkHandlersConfig?.find(state => state.id === SSH_TUNNEL_ID); - const tab = useTab(props.tabId); + const sshPart = getConnectionFormSSHPart(props.formState); - if (!state || !tab.selected) { + useAutoLoad(SSHPanel, sshPart); + + if (!sshPart.state) { return null; } - return ; + return ; }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx index 15c1ff5638..0300c6762c 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx @@ -1,28 +1,28 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled from 'reshadow'; -import { Translate, useResource, useStyles } from '@cloudbeaver/core-blocks'; +import { Translate, useResource } from '@cloudbeaver/core-blocks'; import { NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; -import { Tab, TabContainerTabComponent, TabTitle } from '@cloudbeaver/core-ui'; +import { Tab, type TabContainerTabComponent, TabTitle, useTab } from '@cloudbeaver/core-ui'; +import type { IConnectionFormProps } from '../IConnectionFormState.js'; -import type { IConnectionFormProps } from '../IConnectionFormProps'; +export const SSHTab: TabContainerTabComponent = observer(function SSHTab(props) { + const { selected } = useTab(props.tabId); + const handler = useResource(SSHTab, NetworkHandlerResource, SSH_TUNNEL_ID, { + active: selected, + }); -export const SSHTab: TabContainerTabComponent = observer(function SSHTab({ style, ...rest }) { - const styles = useStyles(style); - const handler = useResource(SSHTab, NetworkHandlerResource, SSH_TUNNEL_ID); - - return styled(styles)( - + return ( + - , + ); }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/authTypes.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/authTypes.ts index e469d4a713..e6e65fc63f 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/authTypes.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/authTypes.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/getConnectionFormSSHPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/getConnectionFormSSHPart.ts new file mode 100644 index 0000000000..1184255c7a --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/getConnectionFormSSHPart.ts @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import type { IFormState } from '@cloudbeaver/core-ui'; +import { ConnectionFormSSHPart } from './ConnectionFormSSHPart.js'; +import { ConnectionInfoNetworkHandlersResource, NetworkHandlerResource } from '@cloudbeaver/core-connections'; +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; + +const DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART = createDataContext('Connection Form SSH Part'); + +export function getConnectionFormSSHPart(formState: IFormState): ConnectionFormSSHPart { + return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const connectionInfoNetworkHandlersResource = di.getService(ConnectionInfoNetworkHandlersResource); + const networkHandlerResource = di.getService(NetworkHandlerResource); + const optionsPart = getConnectionFormOptionsPart(formState); + + return new ConnectionFormSSHPart(formState, networkHandlerResource, connectionInfoNetworkHandlersResource, optionsPart); + }); +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionFormSSLPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionFormSSLPart.ts new file mode 100644 index 0000000000..f1d93c7e83 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionFormSSLPart.ts @@ -0,0 +1,192 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { FormPart, formSubmitContext, type IFormState } from '@cloudbeaver/core-ui'; + +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import { type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; +import { isNotNullDefined } from '@dbeaver/js-helpers'; +import { getSSLDriverHandler } from './getSSLDriverHandler.js'; +import { ConnectionInfoNetworkHandlersResource, type DBDriverResource, type NetworkHandlerResource } from '@cloudbeaver/core-connections'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { makeObservable, observable, toJS } from 'mobx'; +import { PROPERTY_FEATURE_SECURED } from './PROPERTY_FEATURE_SECURED.js'; +import { SSL_CODE_NAME } from './SSL_CODE_NAME.js'; +import type { INetworkHandlerConfig } from '../Options/IConnectionNetworkHanler.js'; +import { getSSLDefaultConfig } from './getSSLDefaultConfig.js'; +import { ConnectionFormOptionsPart } from '../Options/ConnectionFormOptionsPart.js'; + +const getDefaultState = () => + ({ + id: SSL_CODE_NAME, + enabled: false, + properties: {}, + secureProperties: {}, + }) as INetworkHandlerConfig; + +export class ConnectionFormSSLPart extends FormPart { + private activeDriverId: string | undefined; + constructor( + formState: IFormState, + private readonly dbDriverResource: DBDriverResource, + private readonly networkHandlerResource: NetworkHandlerResource, + private readonly connectionInfoNetworkHandlersResource: ConnectionInfoNetworkHandlersResource, + private readonly optionsPart: ConnectionFormOptionsPart, + ) { + super(formState, getDefaultState()); + + this.activeDriverId = undefined; + + makeObservable(this, { + activeDriverId: observable, + }); + } + + override isLoaded(): boolean { + return super.isLoaded() && this.activeDriverId === this.optionsPart.state.driverId; + } + + override isOutdated(): boolean { + const isDriverOutdated = Boolean(this.optionsPart.state.driverId && this.dbDriverResource.isOutdated(this.optionsPart.state.driverId)); + const isNetworkHandlerOutdated = this.networkHandlerResource.isOutdated(CachedMapAllKey); + const isConnectionInfoNetworkHandlersOutdated = this.optionsPart.connectionKey + ? this.connectionInfoNetworkHandlersResource.isOutdated(this.optionsPart.connectionKey) + : false; + + return isDriverOutdated || isNetworkHandlerOutdated || isConnectionInfoNetworkHandlersOutdated; + } + + protected override async loader(): Promise { + this.activeDriverId = this.optionsPart.state.driverId; + + if (!this.optionsPart.state.driverId) { + this.setInitialState(getDefaultState()); + return; + } + + const [driver, handlers] = await Promise.all([ + this.dbDriverResource.load(this.optionsPart.state.driverId), + this.networkHandlerResource.load(CachedMapAllKey), + ]); + const handler = getSSLDriverHandler(handlers, driver?.applicableNetworkHandlers ?? []); + + if (!handler) { + this.setInitialState(getDefaultState()); + return; + } + + const info = this.optionsPart.connectionKey ? await this.connectionInfoNetworkHandlersResource.load(this.optionsPart.connectionKey) : null; + const initialConfig = info?.networkHandlersConfig?.find(h => h.id === handler.id); + + if (!this.optionsPart.state.networkHandlersConfig?.some(state => state.id === handler.id)) { + const config: NetworkHandlerConfigInput = initialConfig ? toJS(initialConfig) : getSSLDefaultConfig(handler.id); + + if (config.secureProperties) { + config.properties = { ...config.properties, ...config.secureProperties }; + } + + this.setInitialState(config); + return; + } + + this.setInitialState(initialConfig ?? getDefaultState()); + } + + protected override async format( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise { + if (!this.optionsPart.state.driverId) { + return; + } + + const handlers = await this.networkHandlerResource.load(CachedMapAllKey); + const descriptor = handlers.find(h => h.id === this.state?.id); + + const handlerConfig: NetworkHandlerConfigInput = toJS(this.state); + handlerConfig.savePassword = this.state.savePassword || this.optionsPart.state.sharedCredentials; + + if (this.isChanged && descriptor) { + for (const descriptorProperty of descriptor.properties) { + if (!descriptorProperty.id) { + continue; + } + + const key = descriptorProperty.id; + const isDefault = isNotNullDefined(descriptorProperty.defaultValue); + + if (!(key in handlerConfig.properties) && isDefault) { + handlerConfig.properties[key] = descriptorProperty.defaultValue; + } + + const secured = descriptorProperty.features.includes(PROPERTY_FEATURE_SECURED); + + if (secured) { + const value = handlerConfig.properties[key]; + const propertyChanged = this.initialState?.secureProperties?.[key] !== value; + + if (propertyChanged) { + handlerConfig.secureProperties[key] = toJS(value); + } else { + delete handlerConfig.secureProperties[key]; + } + + delete handlerConfig.properties[key]; + } + } + + const submitInfo = contexts.getContext(formSubmitContext); + if (submitInfo.type === 'submit') { + if (Object.keys(handlerConfig.secureProperties).length === 0) { + delete handlerConfig.secureProperties; + } + + if (Object.keys(handlerConfig.properties).length === 0) { + delete handlerConfig.properties; + } + } + } + + if (this.state.enabled && !this.state.savePassword) { + this.formState.state.requiredNetworkHandlersIds.push(this.state.id); + } else if (!this.state.enabled) { + this.formState.state.requiredNetworkHandlersIds = this.formState.state.requiredNetworkHandlersIds.filter(id => id !== this.state.id); + } + + if (handlerConfig) { + trimSSLConfig(handlerConfig); + + this.optionsPart.state.networkHandlersConfig!.push(handlerConfig); + } + } + + protected override async saveChanges( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise {} +} + +function trimSSLConfig(input: INetworkHandlerConfig): INetworkHandlerConfig { + const { secureProperties } = input; + + if (!secureProperties) { + return input; + } + + if (!Object.keys(secureProperties).length) { + return input; + } + + for (const key in secureProperties) { + if (typeof secureProperties[key] === 'string') { + secureProperties[key] = secureProperties[key]?.trim(); + } + } + + return input; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts index 5318b35ace..0068a0c9c4 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts @@ -1,64 +1,53 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, makeObservable, toJS } from 'mobx'; import React from 'react'; import { DBDriverResource, NetworkHandlerResource } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import type { NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; -import { formStateContext } from '@cloudbeaver/core-ui'; -import { isNotNullDefined, isObjectsEqual } from '@cloudbeaver/core-utils'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; +import { getSSLDriverHandler } from './getSSLDriverHandler.js'; +import { ConnectionFormService } from '../ConnectionFormService.js'; +import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; -import { connectionFormConfigureContext } from '../connectionFormConfigureContext'; -import { ConnectionFormService } from '../ConnectionFormService'; -import { connectionConfigContext } from '../Contexts/connectionConfigContext'; -import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext'; -import type { IConnectionFormFillConfigData, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps'; -import { getSSLDefaultConfig } from './getSSLDefaultConfig'; -import { getSSLDriverHandler } from './getSSLDriverHandler'; -import { PROPERTY_FEATURE_SECURED } from './PROPERTY_FEATURE_SECURED'; -import { SSL_CODE_NAME } from './SSL_CODE_NAME'; - -export const SSLTab = React.lazy(async () => { - const { SSLTab } = await import('./SSLTab'); +const SSLTab = React.lazy(async () => { + const { SSLTab } = await import('./SSLTab.js'); return { default: SSLTab }; }); -export const SSLPanel = React.lazy(async () => { - const { SSLPanel } = await import('./SSLPanel'); +const SSLPanel = React.lazy(async () => { + const { SSLPanel } = await import('./SSLPanel.js'); return { default: SSLPanel }; }); -@injectable() +@injectable(() => [DBDriverResource, NetworkHandlerResource, ConnectionFormService]) export class ConnectionSSLTabService extends Bootstrap { constructor( - private readonly connectionFormService: ConnectionFormService, private readonly dbDriverResource: DBDriverResource, private readonly networkHandlerResource: NetworkHandlerResource, + private readonly connectionFormService: ConnectionFormService, ) { super(); - - makeObservable(this, { - fillConfig: action, - prepareConfig: action, - }); } - register(): void { - this.connectionFormService.tabsContainer.add({ + override register(): void { + this.connectionFormService.parts.add({ key: 'ssl', order: 4, tab: () => SSLTab, panel: () => SSLPanel, + getLoader: () => [ + getCachedMapResourceLoaderState(this.dbDriverResource, () => CachedMapAllKey), + getCachedMapResourceLoaderState(this.networkHandlerResource, () => CachedMapAllKey), + ], isHidden: (_, props) => { - if (props?.state.config.driverId) { - const driver = this.dbDriverResource.get(props.state.config.driverId); + const optionsPart = props?.formState ? getConnectionFormOptionsPart(props.formState) : null; + + if (optionsPart?.state.driverId) { + const driver = this.dbDriverResource.get(optionsPart.state.driverId); const handler = getSSLDriverHandler(this.networkHandlerResource.values, driver?.applicableNetworkHandlers ?? []); return !handler; } @@ -66,154 +55,5 @@ export class ConnectionSSLTabService extends Bootstrap { return true; }, }); - - this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this)); - - this.connectionFormService.formStateTask.addHandler(this.formState.bind(this)); - - this.connectionFormService.configureTask.addHandler(this.configure.bind(this)); - - this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this)); - } - - load(): void {} - - private async fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider) { - if (!updated || !state.config.driverId) { - return; - } - - const driver = await this.dbDriverResource.load(state.config.driverId); - const handlers = await this.networkHandlerResource.load(CachedMapAllKey); - - const handler = getSSLDriverHandler(handlers, driver?.applicableNetworkHandlers ?? []); - - if (!handler) { - return; - } - - const initialConfig = state.info?.networkHandlersConfig?.find(h => h.id === handler.id); - - if (!state.config.networkHandlersConfig) { - state.config.networkHandlersConfig = []; - } - - if (!state.config.networkHandlersConfig.some(state => state.id === handler.id)) { - const config: NetworkHandlerConfigInput = initialConfig ? toJS(initialConfig) : getSSLDefaultConfig(handler.id); - - if (config.secureProperties) { - config.properties = { ...config.properties, ...config.secureProperties }; - } - - state.config.networkHandlersConfig.push(config); - } - } - - private configure(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const configuration = contexts.getContext(connectionFormConfigureContext); - - configuration.include('includeNetworkHandlersConfig'); - } - - private async prepareConfig({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - const credentialsState = contexts.getContext(connectionCredentialsStateContext); - - if (!state.config.networkHandlersConfig || state.config.networkHandlersConfig.length === 0 || !state.config.driverId) { - return; - } - - const driver = await this.dbDriverResource.load(state.config.driverId); - const handlers = await this.networkHandlerResource.load(CachedMapAllKey); - const handler = state.config.networkHandlersConfig.find( - handler => driver?.applicableNetworkHandlers.includes(handler.id) && handlers.some(h => h.id === handler.id && h.codeName === SSL_CODE_NAME), - ); - const descriptor = handlers.find(h => h.id === handler?.id); - - if (!handler) { - return; - } - - const initial = state.info?.networkHandlersConfig?.find(h => h.id === handler.id); - const handlerConfig: NetworkHandlerConfigInput = toJS(handler); - - const changed = this.isChanged(handlerConfig, initial); - - if (changed && descriptor) { - for (const descriptorProperty of descriptor.properties) { - if (!descriptorProperty.id) { - continue; - } - - const key = descriptorProperty.id; - const isDefault = isNotNullDefined(descriptorProperty.defaultValue); - - if (!(key in handlerConfig.properties) && isDefault) { - handlerConfig.properties[key] = descriptorProperty.defaultValue; - } - - const secured = descriptorProperty.features.includes(PROPERTY_FEATURE_SECURED); - - if (secured) { - const value = handlerConfig.properties[key]; - const propertyChanged = initial?.secureProperties?.[key] !== value; - - if (propertyChanged) { - handlerConfig.secureProperties[key] = toJS(value); - } else { - delete handlerConfig.secureProperties[key]; - } - - delete handlerConfig.properties[key]; - } - } - - if (Object.keys(handlerConfig.secureProperties).length === 0) { - delete handlerConfig.secureProperties; - } - - if (Object.keys(handlerConfig.properties).length === 0) { - delete handlerConfig.properties; - } - } - - if (handler.enabled && !handler.savePassword) { - credentialsState.requireNetworkHandler(handler.id); - } - - if (changed) { - if (!config.networkHandlersConfig) { - config.networkHandlersConfig = []; - } - - config.networkHandlersConfig.push(handlerConfig); - } - } - - private formState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - if (config.networkHandlersConfig !== undefined) { - const stateContext = contexts.getContext(formStateContext); - - stateContext.markEdited(); - } - } - - private isChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) { - if (!initial && !handler.enabled) { - return false; - } - - const initialProperties = { ...(initial?.properties ?? {}), ...(initial?.secureProperties ?? {}) }; - - if ( - handler.enabled !== initial?.enabled || - handler.savePassword !== initial?.savePassword || - !isObjectsEqual(handler.properties, initialProperties) - ) { - return true; - } - - return false; } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/IConnectionFromSSLState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/IConnectionFromSSLState.ts new file mode 100644 index 0000000000..07ed0a5ae7 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/IConnectionFromSSLState.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { schema } from '@cloudbeaver/core-utils'; + +export const CONNECTION_FORM_SSL_SCHEMA = schema.object({}); + +export type IConnectionFromSSLState = schema.infer; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/PROPERTY_FEATURE_SECURED.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/PROPERTY_FEATURE_SECURED.ts index 186d2e9066..e3979ac514 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/PROPERTY_FEATURE_SECURED.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/PROPERTY_FEATURE_SECURED.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SAVED_VALUE_INDICATOR.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SAVED_VALUE_INDICATOR.ts index 7ee367fa3d..ca20eda135 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SAVED_VALUE_INDICATOR.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SAVED_VALUE_INDICATOR.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.module.css b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.module.css new file mode 100644 index 0000000000..baa382f6c2 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.module.css @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.form { + display: flex; + flex-direction: column; + flex: 1; + overflow: auto; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx index 0935e9c0b3..b3524ea6e3 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 React from 'react'; -import styled, { css } from 'reshadow'; import { ColoredContainer, @@ -15,61 +15,74 @@ import { Form, Group, GroupTitle, + IconOrImage, ObjectPropertyInfoForm, + s, Switch, useAdministrationSettings, + useAutoLoad, useObjectPropertyCategories, - useStyles, + useResource, + useS, useTranslate, } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; import type { NetworkHandlerConfigInput, NetworkHandlerDescriptor } from '@cloudbeaver/core-sdk'; -import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; +import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; import { isSafari } from '@cloudbeaver/core-utils'; +import { WEBSITE_LINKS } from '@cloudbeaver/core-links'; -import type { IConnectionFormProps } from '../IConnectionFormProps'; -import { SAVED_VALUE_INDICATOR } from './SAVED_VALUE_INDICATOR'; - -const SSl_STYLES = css` - Form { - display: flex; - flex-direction: column; - flex: 1; - overflow: auto; - } -`; +import { SAVED_VALUE_INDICATOR } from './SAVED_VALUE_INDICATOR.js'; +import styles from './SSL.module.css'; +import type { IConnectionFormProps } from '../IConnectionFormState.js'; +import { ConnectionInfoNetworkHandlersResource } from '@cloudbeaver/core-connections'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; +import { SSLDescription } from './SSLDescription.js'; interface Props extends IConnectionFormProps { handler: NetworkHandlerDescriptor; handlerState: NetworkHandlerConfigInput; } -export const SSL: TabContainerPanelComponent = observer(function SSL({ state: formState, handler, handlerState }) { - const { info, readonly, disabled: formDisabled, loading } = formState; - +export const SSL: TabContainerPanelComponent = observer(function SSL({ formState, handler, handlerState, tabId }) { const translate = useTranslate(); - - const styles = useStyles(SSl_STYLES); + const { selected } = useTab(tabId); + const style = useS(styles); const { credentialsSavingEnabled } = useAdministrationSettings(); const { categories, isUncategorizedExists } = useObjectPropertyCategories(handler.properties); + const serverConfigResource = useResource(SSL, ServerConfigResource, undefined, { + active: selected, + }); - const disabled = formDisabled || loading; + const disabled = formState.isDisabled || formState.isReadOnly; const enabled = handlerState.enabled || false; - const initialHandler = info?.networkHandlersConfig?.find(h => h.id === handler.id); + const optionsPart = getConnectionFormOptionsPart(formState); + const connectionInfoNetworkHandlersService = useResource(SSL, ConnectionInfoNetworkHandlersResource, optionsPart.connectionKey, { + active: selected, + }); + const handlersInfo = connectionInfoNetworkHandlersService.data; + const initialHandler = handlersInfo?.networkHandlersConfig?.find(h => h.id === handler.id); const autofillToken = isSafari ? 'section-connection-authentication-ssl section-ssl' : 'new-password'; + const projectInfoResource = useService(ProjectInfoResource); + const isSharedProject = projectInfoResource.isProjectShared(formState.state.projectId); - return styled(styles)( -
+ useAutoLoad(SSL, optionsPart, enabled); + + return ( + - - {translate('connections_public_connection_ssl_enable')} + } mod={['primary']} disabled={disabled}> + {translate('plugin_connections_connection_ssl_enable')} {isUncategorizedExists && ( !!p.id && initialHandler?.secureProperties[p.id] === SAVED_VALUE_INDICATOR} autofillToken={autofillToken} hideEmptyPlaceholder @@ -77,7 +90,6 @@ export const SSL: TabContainerPanelComponent = observer(function SSL({ st small /> )} - {categories.map(category => ( {category} @@ -85,7 +97,7 @@ export const SSL: TabContainerPanelComponent = observer(function SSL({ st state={handlerState.properties} properties={handler.properties} category={category} - disabled={disabled || readonly || !enabled} + disabled={disabled || !enabled} isSaved={p => !!p.id && initialHandler?.secureProperties[p.id] === SAVED_VALUE_INDICATOR} autofillToken={autofillToken} hideEmptyPlaceholder @@ -94,19 +106,36 @@ export const SSL: TabContainerPanelComponent = observer(function SSL({ st /> ))} - - {credentialsSavingEnabled && !formState.config.template && ( + {credentialsSavingEnabled && !optionsPart.state.sharedCredentials && ( - {translate('connections_connection_edit_save_credentials')} + {translate( + !isSharedProject || serverConfigResource.data?.distributed + ? 'connections_connection_authentication_save_credentials_for_user' + : 'connections_connection_edit_save_credentials_shared', + )} )} + + + {translate('plugin_connections_connection_ssl_docs')} + -
, + ); }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLDescription.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLDescription.tsx new file mode 100644 index 0000000000..c6feda57f0 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLDescription.tsx @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { IconOrImage, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { ProductInfoResource } from '@cloudbeaver/core-root'; +import type { ReactNode } from 'react'; + +export function SSLDescription(): ReactNode { + const translate = useTranslate(); + const productInfoResource = useResource(SSLDescription, ProductInfoResource, undefined); + const productName = productInfoResource.data?.name || 'CloudBeaver'; + + return ( + <> +
{translate('plugin_connections_connection_ssl_optional')}
+
{translate('plugin_connections_connection_ssl_description')}
+
+ {translate('plugin_connections_connection_ssl_note', undefined, { productName })} +
+ + ); +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLPanel.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLPanel.tsx index 37845646a5..7ef6594963 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLPanel.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLPanel.tsx @@ -1,38 +1,36 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useResource } from '@cloudbeaver/core-blocks'; +import { useAutoLoad, useResource } from '@cloudbeaver/core-blocks'; import { DBDriverResource, NetworkHandlerResource } from '@cloudbeaver/core-connections'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { TabContainerTabComponent, useTab } from '@cloudbeaver/core-ui'; +import type { TabContainerTabComponent } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '../IConnectionFormProps'; -import { getSSLDefaultConfig } from './getSSLDefaultConfig'; -import { getSSLDriverHandler } from './getSSLDriverHandler'; -import { SSL } from './SSL'; +import { getSSLDriverHandler } from './getSSLDriverHandler.js'; +import { SSL } from './SSL.js'; +import { getConnectionFormSSLPart } from './getConnectionFormSSLPart.js'; +import type { IConnectionFormProps } from '../IConnectionFormState.js'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; export const SSLPanel: TabContainerTabComponent = observer(function SSLPanel(props) { - const tab = useTab(props.tabId); const networkHandlerResource = useResource(SSLPanel, NetworkHandlerResource, CachedMapAllKey); - const dbDriverResource = useResource(SSLPanel, DBDriverResource, props.state.config.driverId ?? null); + const optionsPart = getConnectionFormOptionsPart(props.formState); + const dbDriverResource = useResource(SSLPanel, DBDriverResource, optionsPart.state.driverId ?? null); const handler = getSSLDriverHandler(networkHandlerResource.resource.values, dbDriverResource.data?.applicableNetworkHandlers ?? []); + const sslPart = getConnectionFormSSLPart(props.formState); - if (props.state.configured && handler && !props.state.config.networkHandlersConfig?.some(state => state.id === handler?.id)) { - props.state.config.networkHandlersConfig?.push(getSSLDefaultConfig(handler.id)); - } - - const handlerState = props.state.config.networkHandlersConfig?.find(h => h.id === handler?.id); + useAutoLoad(SSLPanel, sslPart); - if (!handler || !handlerState || !tab.selected) { + if (!handler || !sslPart.state) { return null; } - return ; + return ; }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLTab.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLTab.tsx index 3f24fffd40..5a3dbfba5b 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLTab.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLTab.tsx @@ -1,25 +1,29 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled from 'reshadow'; -import { Translate, useResource, useStyles } from '@cloudbeaver/core-blocks'; +import { Translate, useResource } from '@cloudbeaver/core-blocks'; import { DBDriverResource, NetworkHandlerResource } from '@cloudbeaver/core-connections'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { Tab, TabContainerTabComponent, TabTitle } from '@cloudbeaver/core-ui'; +import { Tab, type TabContainerTabComponent, TabTitle, useTab } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '../IConnectionFormProps'; -import { getSSLDriverHandler } from './getSSLDriverHandler'; +import { getSSLDriverHandler } from './getSSLDriverHandler.js'; +import type { IConnectionFormProps } from '../IConnectionFormState.js'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; -export const SSLTab: TabContainerTabComponent = observer(function SSLTab({ style, ...rest }) { - const styles = useStyles(style); - const networkHandlerResource = useResource(SSLTab, NetworkHandlerResource, CachedMapAllKey); - const dbDriverResource = useResource(SSLTab, DBDriverResource, rest.state.config.driverId ?? null); +export const SSLTab: TabContainerTabComponent = observer(function SSLTab(props) { + const { selected } = useTab(props.tabId); + const networkHandlerResource = useResource(SSLTab, NetworkHandlerResource, CachedMapAllKey, { + active: selected, + }); + const optionsPart = getConnectionFormOptionsPart(props.formState); + const dbDriverResource = useResource(SSLTab, DBDriverResource, optionsPart.state.driverId ?? null); const handler = getSSLDriverHandler(networkHandlerResource.resource.values, dbDriverResource.data?.applicableNetworkHandlers ?? []); @@ -27,11 +31,11 @@ export const SSLTab: TabContainerTabComponent = observer(f return null; } - return styled(styles)( - + return ( + - , + ); }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL_CODE_NAME.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL_CODE_NAME.ts index 3d018fb802..85748d6aab 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL_CODE_NAME.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL_CODE_NAME.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getConnectionFormSSLPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getConnectionFormSSLPart.ts new file mode 100644 index 0000000000..6c9aa82c8a --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getConnectionFormSSLPart.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import type { IFormState } from '@cloudbeaver/core-ui'; +import { ConnectionFormSSLPart } from './ConnectionFormSSLPart.js'; +import type { IConnectionFormState } from '../IConnectionFormState.js'; +import { ConnectionInfoNetworkHandlersResource, DBDriverResource, NetworkHandlerResource } from '@cloudbeaver/core-connections'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; + +const DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART = createDataContext('Connection Form SSL Part'); + +export function getConnectionFormSSLPart(formState: IFormState): ConnectionFormSSLPart { + return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const dbDriverResource = di.getService(DBDriverResource); + const networkHandlerResource = di.getService(NetworkHandlerResource); + const connectionInfoNetworkHandlersResource = di.getService(ConnectionInfoNetworkHandlersResource); + const optionsPart = getConnectionFormOptionsPart(formState); + + return new ConnectionFormSSLPart(formState, dbDriverResource, networkHandlerResource, connectionInfoNetworkHandlersResource, optionsPart); + }); +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getSSLDefaultConfig.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getSSLDefaultConfig.ts index 4080b545a3..2d40449bec 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getSSLDefaultConfig.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getSSLDefaultConfig.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getSSLDriverHandler.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getSSLDriverHandler.ts index c9d6693848..0b78f3b1c8 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getSSLDriverHandler.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getSSLDriverHandler.ts @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { NetworkHandlerDescriptor } from '@cloudbeaver/core-sdk'; -import { SSL_CODE_NAME } from './SSL_CODE_NAME'; +import { SSL_CODE_NAME } from './SSL_CODE_NAME.js'; export function getSSLDriverHandler(descriptors: NetworkHandlerDescriptor[], applicableHandlers: string[]) { const result = descriptors.find(descriptor => applicableHandlers.includes(descriptor.id) && descriptor.codeName === SSL_CODE_NAME); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID.ts new file mode 100644 index 0000000000..e5ea0bb354 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID.ts @@ -0,0 +1,9 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export const CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID = 'shared_credentials'; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/connectionFormConfigureContext.ts b/webapp/packages/plugin-connections/src/ConnectionForm/connectionFormConfigureContext.ts deleted file mode 100644 index ee682dd27e..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionForm/connectionFormConfigureContext.ts +++ /dev/null @@ -1,41 +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 type { DatabaseConnection } from '@cloudbeaver/core-connections'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import type { CachedResourceIncludeArgs } from '@cloudbeaver/core-resource'; -import type { GetUserConnectionsQueryVariables } from '@cloudbeaver/core-sdk'; - -import type { IConnectionFormState } from './IConnectionFormProps'; - -export type ConnectionFormInfoIncludes = CachedResourceIncludeArgs; - -export interface IConnectionFormConfigureContext { - readonly driverId: string | undefined; - readonly info: DatabaseConnection | undefined; - readonly connectionIncludes: ConnectionFormInfoIncludes; - - include: (...includes: ConnectionFormInfoIncludes) => any; -} - -export function connectionFormConfigureContext( - contexts: IExecutionContextProvider, - state: IConnectionFormState, -): IConnectionFormConfigureContext { - return { - info: state.info, - driverId: state.config.driverId, - connectionIncludes: [], - include(...includes) { - for (const include of includes) { - if (!this.connectionIncludes.includes(include as never)) { - this.connectionIncludes.push(include as never); - } - } - }, - }; -} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormState.ts index 68751a78f6..7c163a1f54 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormState.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormState.ts @@ -1,34 +1,47 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useEffect, useState } from 'react'; +import { useEffect, useRef } from 'react'; +import { IServiceProvider, useService } from '@cloudbeaver/core-di'; +import { ConnectionFormState } from './ConnectionFormState.js'; +import { ConnectionFormService } from './ConnectionFormService.js'; +import type { IConnectionFormState } from './IConnectionFormState.js'; +import type { IConnectionInfoParams } from '@cloudbeaver/core-connections'; -import type { ConnectionInfoResource } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; - -import { ConnectionFormService } from './ConnectionFormService'; -import { ConnectionFormState } from './ConnectionFormState'; -import type { IConnectionFormState } from './IConnectionFormProps'; - -export function useConnectionFormState(resource: ConnectionInfoResource, configure?: (state: IConnectionFormState) => any): IConnectionFormState { - const projectsService = useService(ProjectsService); - const projectInfoResource = useService(ProjectInfoResource); +const EMPTY_CONNECTION_INFO_PARAMS: IConnectionFormState = { + projectId: '', + availableDrivers: [], + type: 'admin', + requiredNetworkHandlersIds: [], + connectionId: undefined, +}; +export function useConnectionFormState(params: IConnectionInfoParams, configure?: (state: ConnectionFormState) => any): ConnectionFormState { + const serviceProvider = useService(IServiceProvider); const service = useService(ConnectionFormService); - const [state] = useState(() => { - const state = new ConnectionFormState(projectsService, projectInfoResource, service, resource); - configure?.(state); + const ref = useRef(null); + + if (ref.current?.state.connectionId !== params.connectionId || ref.current?.state.projectId !== params.projectId) { + ref.current?.dispose(); + ref.current = new ConnectionFormState(serviceProvider, service, { + ...EMPTY_CONNECTION_INFO_PARAMS, + projectId: params.projectId, + connectionId: params.connectionId, + }); - state.load(); - return state; - }); + configure?.(ref.current); + } - useEffect(() => () => state.dispose(), []); + useEffect( + () => () => { + ref.current?.dispose(); + }, + [], + ); - return state; + return ref.current; } diff --git a/webapp/packages/plugin-connections/src/ConnectionShield.tsx b/webapp/packages/plugin-connections/src/ConnectionShield.tsx new file mode 100644 index 0000000000..cffab2532a --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionShield.tsx @@ -0,0 +1,56 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { type PropsWithChildren } from 'react'; + +import { Button, getComputed, Loader, TextPlaceholder, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, ConnectionsManagerService, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; + +export interface IConnectionShieldProps { + connectionKey: IConnectionInfoParams | null; +} + +export const ConnectionShield = observer>(function ConnectionShield({ connectionKey, children }) { + const translate = useTranslate(); + const connectionsManagerService = useService(ConnectionsManagerService); + const notificationService = useService(NotificationService); + + const connection = useResource(ConnectionShield, ConnectionInfoResource, connectionKey); + const connecting = getComputed(() => (connectionKey && connection.resource.isConnecting(connectionKey)) || connection.isLoading()); + const isConnectionReady = getComputed(() => !connecting && connection.data?.connected && connection.isLoaded() && !connection.isOutdated()); + + async function handleConnect() { + if (isConnectionReady || !connection.data || !connectionKey) { + return; + } + + try { + await connectionsManagerService.requireConnection(connectionKey); + } catch (exception: any) { + notificationService.logException(exception); + } + } + + if (connecting) { + return ; + } + + if (!isConnectionReady) { + return ( + + + + ); + } + + return children; +}); diff --git a/webapp/packages/plugin-connections/src/ConnectionShieldLazy.ts b/webapp/packages/plugin-connections/src/ConnectionShieldLazy.ts new file mode 100644 index 0000000000..195e5b6176 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionShieldLazy.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const ConnectionShieldLazy = importLazyComponent(() => import('./ConnectionShield.js').then(m => m.ConnectionShield)); diff --git a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_CHANGE_CREDENTIALS.ts b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_CHANGE_CREDENTIALS.ts index 2d186dcbb0..c12db2cf3c 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_CHANGE_CREDENTIALS.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_CHANGE_CREDENTIALS.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_DISCONNECT.ts b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_DISCONNECT.ts index dbc0f0ca9c..a2b43ecfcc 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_DISCONNECT.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_DISCONNECT.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_DISCONNECT_ALL.ts b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_DISCONNECT_ALL.ts index 228b206828..78be8b5575 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_DISCONNECT_ALL.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_DISCONNECT_ALL.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_EDIT.ts b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_EDIT.ts index aab5957a4d..590a8000b8 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_EDIT.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_EDIT.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,5 +8,5 @@ import { createAction } from '@cloudbeaver/core-view'; export const ACTION_CONNECTION_EDIT = createAction('connection-edit', { - label: 'connections_public_connection_edit_menu_item_title', + label: 'plugin_connections_connection_edit_menu_item_title', }); diff --git a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_ADVANCED.ts b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_ADVANCED.ts index fe8ff3f5b6..bb06bd3b19 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_ADVANCED.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_ADVANCED.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_SIMPLE.ts b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_SIMPLE.ts index dda3624e25..e3778facb3 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_SIMPLE.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_SIMPLE.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS.ts b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS.ts index 818fa53fad..bd31423627 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/Actions/ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts b/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts index 15f10c5f5a..595a24867c 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { EAdminPermission } from '@cloudbeaver/core-authentication'; import { - Connection, + type Connection, + ConnectionInfoAuthPropertiesResource, ConnectionInfoResource, ConnectionsManagerService, ConnectionsSettingsService, @@ -20,32 +20,46 @@ import { DATA_CONTEXT_NAV_NODE, EObjectFeature, NavNodeManagerService } from '@c import { getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; import { CONNECTION_NAVIGATOR_VIEW_SETTINGS, + EAdminPermission, isNavigatorViewSettingsEqual, - NavigatorViewSettings, + type NavigatorViewSettings, PermissionsService, ServerConfigResource, } from '@cloudbeaver/core-root'; -import { ACTION_DELETE, ActionService, DATA_CONTEXT_MENU, DATA_CONTEXT_MENU_NESTED, MenuSeparatorItem, MenuService } from '@cloudbeaver/core-view'; +import { ACTION_DELETE, ActionService, MenuSeparatorItem, MenuService } from '@cloudbeaver/core-view'; import { MENU_APP_ACTIONS } from '@cloudbeaver/plugin-top-app-bar'; -import { ConnectionAuthService } from '../ConnectionAuthService'; -import { PluginConnectionsSettingsService } from '../PluginConnectionsSettingsService'; -import { PublicConnectionFormService } from '../PublicConnectionForm/PublicConnectionFormService'; -import { ACTION_CONNECTION_CHANGE_CREDENTIALS } from './Actions/ACTION_CONNECTION_CHANGE_CREDENTIALS'; -import { ACTION_CONNECTION_DISCONNECT } from './Actions/ACTION_CONNECTION_DISCONNECT'; -import { ACTION_CONNECTION_DISCONNECT_ALL } from './Actions/ACTION_CONNECTION_DISCONNECT_ALL'; -import { ACTION_CONNECTION_EDIT } from './Actions/ACTION_CONNECTION_EDIT'; -import { ACTION_CONNECTION_VIEW_ADVANCED } from './Actions/ACTION_CONNECTION_VIEW_ADVANCED'; -import { ACTION_CONNECTION_VIEW_SIMPLE } from './Actions/ACTION_CONNECTION_VIEW_SIMPLE'; -import { ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS } from './Actions/ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS'; -import { MENU_CONNECTION_VIEW } from './MENU_CONNECTION_VIEW'; -import { MENU_CONNECTIONS } from './MENU_CONNECTIONS'; - -@injectable() +import { PluginConnectionsSettingsService } from '../PluginConnectionsSettingsService.js'; +import { PublicConnectionFormService } from '../PublicConnectionForm/PublicConnectionFormService.js'; +import { ACTION_CONNECTION_CHANGE_CREDENTIALS } from './Actions/ACTION_CONNECTION_CHANGE_CREDENTIALS.js'; +import { ACTION_CONNECTION_DISCONNECT } from './Actions/ACTION_CONNECTION_DISCONNECT.js'; +import { ACTION_CONNECTION_DISCONNECT_ALL } from './Actions/ACTION_CONNECTION_DISCONNECT_ALL.js'; +import { ACTION_CONNECTION_EDIT } from './Actions/ACTION_CONNECTION_EDIT.js'; +import { ACTION_CONNECTION_VIEW_ADVANCED } from './Actions/ACTION_CONNECTION_VIEW_ADVANCED.js'; +import { ACTION_CONNECTION_VIEW_SIMPLE } from './Actions/ACTION_CONNECTION_VIEW_SIMPLE.js'; +import { ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS } from './Actions/ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS.js'; +import { MENU_CONNECTION_VIEW } from './MENU_CONNECTION_VIEW.js'; +import { MENU_CONNECTIONS } from './MENU_CONNECTIONS.js'; + +@injectable(() => [ + NotificationService, + ConnectionInfoResource, + ConnectionInfoAuthPropertiesResource, + NavNodeManagerService, + ConnectionsManagerService, + ActionService, + MenuService, + PublicConnectionFormService, + ConnectionsSettingsService, + PluginConnectionsSettingsService, + PermissionsService, + ServerConfigResource, +]) export class ConnectionMenuBootstrap extends Bootstrap { constructor( private readonly notificationService: NotificationService, private readonly connectionInfoResource: ConnectionInfoResource, + private readonly connectionInfoAuthPropertiesResource: ConnectionInfoAuthPropertiesResource, private readonly navNodeManagerService: NavNodeManagerService, private readonly connectionsManagerService: ConnectionsManagerService, private readonly actionService: ActionService, @@ -54,47 +68,31 @@ export class ConnectionMenuBootstrap extends Bootstrap { private readonly connectionsSettingsService: ConnectionsSettingsService, private readonly pluginConnectionsSettingsService: PluginConnectionsSettingsService, private readonly permissionsService: PermissionsService, - private readonly connectionAuthService: ConnectionAuthService, private readonly serverConfigResource: ServerConfigResource, ) { super(); } - register(): void | Promise { + override register(): void { this.addConnectionsMenuToTopAppBar(); this.menuService.addCreator({ + root: true, + contexts: [DATA_CONTEXT_CONNECTION, DATA_CONTEXT_NAV_NODE], isApplicable: context => { - if ( - this.pluginConnectionsSettingsService.settings.getValue('hideConnectionViewForUsers') && - !this.permissionsService.has(EAdminPermission.admin) - ) { + if (this.pluginConnectionsSettingsService.hideConnectionViewForUsers && !this.permissionsService.has(EAdminPermission.admin)) { return false; } - const connection = context.tryGet(DATA_CONTEXT_CONNECTION); - - if (!connection?.connected) { - return false; - } - - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); - - if (node && !node.objectFeatures.includes(EObjectFeature.dataSource)) { - return false; - } + const node = context.get(DATA_CONTEXT_NAV_NODE)!; - return context.has(DATA_CONTEXT_CONNECTION) && !context.has(DATA_CONTEXT_MENU_NESTED); + return node.objectFeatures.includes(EObjectFeature.dataSource) && node.objectFeatures.includes(EObjectFeature.dataSourceConnected); }, getItems: (context, items) => [...items, MENU_CONNECTION_VIEW], }); this.menuService.addCreator({ - isApplicable: context => { - const menu = context.tryGet(DATA_CONTEXT_MENU); - - return menu === MENU_CONNECTION_VIEW; - }, + menus: [MENU_CONNECTION_VIEW], getItems: (context, items) => [ ...items, ACTION_CONNECTION_VIEW_SIMPLE, @@ -106,10 +104,15 @@ export class ConnectionMenuBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'connection-view', - isActionApplicable: (context, action) => - [ACTION_CONNECTION_VIEW_SIMPLE, ACTION_CONNECTION_VIEW_ADVANCED, ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS].includes(action), + actions: [ACTION_CONNECTION_VIEW_SIMPLE, ACTION_CONNECTION_VIEW_ADVANCED, ACTION_CONNECTION_VIEW_SYSTEM_OBJECTS], + contexts: [DATA_CONTEXT_CONNECTION], isChecked: (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION); + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + const connection = this.connectionInfoResource.get(connectionKey); + + if (!connection) { + return false; + } switch (action) { case ACTION_CONNECTION_VIEW_SIMPLE: { @@ -126,7 +129,8 @@ export class ConnectionMenuBootstrap extends Bootstrap { return false; }, handler: async (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION); + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + const connection = await this.connectionInfoResource.load(connectionKey); switch (action) { case ACTION_CONNECTION_VIEW_SIMPLE: { @@ -148,10 +152,15 @@ export class ConnectionMenuBootstrap extends Bootstrap { } } }, + getLoader: context => { + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + return getCachedMapResourceLoaderState(this.connectionInfoResource, () => connectionKey, undefined, true); + }, }); this.menuService.addCreator({ - isApplicable: context => context.has(DATA_CONTEXT_CONNECTION) && !context.has(DATA_CONTEXT_MENU_NESTED), + root: true, + contexts: [DATA_CONTEXT_CONNECTION], getItems: (context, items) => [ ...items, ACTION_CONNECTION_CHANGE_CREDENTIALS, @@ -163,65 +172,61 @@ export class ConnectionMenuBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'connection-management', - isActionApplicable: (context, action) => { - const connection = context.tryGet(DATA_CONTEXT_CONNECTION); + actions: [ + ACTION_DELETE, + ACTION_CONNECTION_CHANGE_CREDENTIALS, + ACTION_CONNECTION_EDIT, + ACTION_CONNECTION_DISCONNECT, + ACTION_CONNECTION_DISCONNECT_ALL, + ], + contexts: [DATA_CONTEXT_CONNECTION, DATA_CONTEXT_NAV_NODE], + isActionApplicable: context => { + const node = context.get(DATA_CONTEXT_NAV_NODE)!; - if (!connection) { - return false; - } - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); + return node.objectFeatures.includes(EObjectFeature.dataSource); + }, + isHidden: (context, action) => { + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + const connection = this.connectionInfoResource.get(connectionKey); - if (node && !node.objectFeatures.includes(EObjectFeature.dataSource)) { - return false; + if (!connection) { + return true; } if (action === ACTION_CONNECTION_DISCONNECT) { - return connection.connected; + return !connection.connected; } if (action === ACTION_CONNECTION_DISCONNECT_ALL) { - return this.connectionsManagerService.hasAnyConnection(true); + return !this.connectionsManagerService.hasAnyConnection(true); } if (action === ACTION_DELETE) { - return connection.canDelete; + return !connection.canDelete; } if (action === ACTION_CONNECTION_EDIT) { - return connection.canEdit || connection.canViewSettings; + return !(connection.canEdit || connection.canViewSettings); } if (action === ACTION_CONNECTION_CHANGE_CREDENTIALS) { - return this.serverConfigResource.distributed; + const auth = this.connectionInfoAuthPropertiesResource.get(connectionKey); + return !this.serverConfigResource.distributed || !!auth?.sharedCredentials; } - return false; - }, - isHidden: (context, action) => { - const connection = context.tryGet(DATA_CONTEXT_CONNECTION); - - if (action === ACTION_CONNECTION_CHANGE_CREDENTIALS) { - return !connection?.credentialsSaved; - } - - return false; + return true; }, getLoader: (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION); - + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; if (action === ACTION_CONNECTION_CHANGE_CREDENTIALS) { - return getCachedMapResourceLoaderState( - this.connectionInfoResource, - () => createConnectionParam(connection), - () => ['includeCredentialsSaved' as const], - true, - ); + return getCachedMapResourceLoaderState(this.connectionInfoAuthPropertiesResource, () => connectionKey, undefined, true); } - return []; + return getCachedMapResourceLoaderState(this.connectionInfoResource, () => connectionKey, undefined, true); }, handler: async (context, action) => { - const connection = context.get(DATA_CONTEXT_CONNECTION); + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + const connection = await this.connectionInfoResource.load(connectionKey); switch (action) { case ACTION_CONNECTION_DISCONNECT: { @@ -245,7 +250,7 @@ export class ConnectionMenuBootstrap extends Bootstrap { break; } case ACTION_CONNECTION_CHANGE_CREDENTIALS: { - await this.connectionAuthService.auth({ connectionId: connection.id, projectId: connection.projectId }, true); + await this.connectionsManagerService.requireConnection({ connectionId: connection.id, projectId: connection.projectId }, true); break; } } @@ -253,14 +258,12 @@ export class ConnectionMenuBootstrap extends Bootstrap { }); } - load(): void {} - private async changeConnectionView(connection: Connection, settings: NavigatorViewSettings) { try { connection = await this.connectionInfoResource.changeConnectionView(createConnectionParam(connection), settings); if (connection.nodePath) { - await this.navNodeManagerService.refreshTree(connection.nodePath); + await this.navNodeManagerService.refreshNode(connection.nodePath); } } catch (exception: any) { this.notificationService.logException(exception); @@ -274,9 +277,8 @@ export class ConnectionMenuBootstrap extends Bootstrap { }); this.menuService.setHandler({ id: 'connections-menu-base', - isApplicable: context => context.tryGet(DATA_CONTEXT_MENU) === MENU_CONNECTIONS, - isHidden: () => - this.connectionsManagerService.createConnectionProjects.length === 0 || this.connectionsSettingsService.settings.getValue('disabled'), + menus: [MENU_CONNECTIONS], + isHidden: () => this.connectionsManagerService.createConnectionProjects.length === 0 || this.connectionsSettingsService.disabled, isLabelVisible: () => false, }); } diff --git a/webapp/packages/plugin-connections/src/ContextMenu/MENU_CONNECTIONS.ts b/webapp/packages/plugin-connections/src/ContextMenu/MENU_CONNECTIONS.ts index c88d42439e..1fc6f25dbd 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/MENU_CONNECTIONS.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/MENU_CONNECTIONS.ts @@ -1,15 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const MENU_CONNECTIONS = createMenu( - 'connections', - 'plugin_connections_menu_connections_label', - '/icons/plugin_connections_menu_m.svg', - 'plugin_connections_menu_connections_label', -); +export const MENU_CONNECTIONS = createMenu('connections', { + label: 'plugin_connections_menu_connections_label', + icon: '/icons/plugin_connections_menu_m.svg', + tooltip: 'plugin_connections_menu_connections_label', +}); diff --git a/webapp/packages/plugin-connections/src/ContextMenu/MENU_CONNECTION_VIEW.ts b/webapp/packages/plugin-connections/src/ContextMenu/MENU_CONNECTION_VIEW.ts index 45f785c3f0..36e2ec4bfe 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/MENU_CONNECTION_VIEW.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/MENU_CONNECTION_VIEW.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const MENU_CONNECTION_VIEW = createMenu('connection-view', 'app_navigationTree_connection_view'); +export const MENU_CONNECTION_VIEW = createMenu('connection-view', { label: 'app_navigationTree_connection_view' }); diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogController.ts b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogController.ts deleted file mode 100644 index df6c939394..0000000000 --- a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogController.ts +++ /dev/null @@ -1,155 +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 { makeObservable, observable } from 'mobx'; - -import { ErrorDetailsDialog } from '@cloudbeaver/core-blocks'; -import { - ConnectionInfoResource, - ConnectionInitConfig, - DBDriverResource, - IConnectionInfoParams, - USER_NAME_PROPERTY_ID, -} from '@cloudbeaver/core-connections'; -import { IDestructibleController, IInitializableController, injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { GQLErrorCatcher, NetworkHandlerAuthType } from '@cloudbeaver/core-sdk'; - -import type { IConnectionAuthenticationConfig } from '../ConnectionAuthentication/IConnectionAuthenticationConfig'; - -@injectable() -export class DBAuthDialogController implements IInitializableController, IDestructibleController { - isAuthenticating = false; - configured = false; - config: IConnectionAuthenticationConfig = { - credentials: {}, - networkHandlersConfig: [], - saveCredentials: false, - }; - - readonly error = new GQLErrorCatcher(); - - private connectionKey!: IConnectionInfoParams; - private isDistructed = false; - private networkHandlers!: string[]; - private close!: () => void; - - constructor( - private readonly notificationService: NotificationService, - private readonly connectionInfoResource: ConnectionInfoResource, - private readonly commonDialogService: CommonDialogService, - private readonly dbDriverResource: DBDriverResource, - ) { - makeObservable(this, { - isAuthenticating: observable.ref, - configured: observable.ref, - config: observable, - }); - } - - async init(connectionKey: IConnectionInfoParams, networkHandlers: string[], onClose: () => void): Promise { - this.connectionKey = connectionKey; - this.networkHandlers = networkHandlers; - this.close = onClose; - - await this.loadAuthModel(); - this.loadDrivers(); - - this.configured = true; - } - - destruct(): void { - this.isDistructed = true; - } - - login = async (): Promise => { - if (this.isAuthenticating) { - return; - } - - this.isAuthenticating = true; - try { - await this.connectionInfoResource.init(this.getConfig()); - this.close(); - } catch (exception: any) { - if (!this.error.catch(exception) || this.isDistructed) { - this.notificationService.logException(exception, 'Authentication failed'); - } - } finally { - this.isAuthenticating = false; - } - }; - - showDetails = (): void => { - if (this.error.exception) { - this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); - } - }; - - private getConfig() { - const config: ConnectionInitConfig = { - projectId: this.connectionKey.projectId, - connectionId: this.connectionKey.connectionId, - }; - - if (Object.keys(this.config.credentials).length > 0) { - config.credentials = this.config.credentials; - } - - config.saveCredentials = this.config.saveCredentials; - - if (this.config.networkHandlersConfig.length > 0) { - config.networkCredentials = this.config.networkHandlersConfig; - } - - return config; - } - - private async loadAuthModel() { - try { - const connection = await this.connectionInfoResource.load(this.connectionKey, [ - 'includeAuthProperties', - 'includeNetworkHandlersConfig', - 'includeAuthNeeded', - ]); - - if (connection.authNeeded) { - const property = connection.authProperties.find(property => property.id === USER_NAME_PROPERTY_ID); - - if (property?.value) { - this.config.credentials[USER_NAME_PROPERTY_ID] = property.value; - } - } - - for (const id of this.networkHandlers) { - const handler = connection.networkHandlersConfig.find(handler => handler.id === id); - - if (handler && (handler.userName || handler.authType !== NetworkHandlerAuthType.Password)) { - this.config.networkHandlersConfig.push({ - id: handler.id, - authType: handler.authType, - userName: handler.userName, - password: handler.password, - savePassword: handler.savePassword, - }); - } - } - } catch (exception: any) { - this.notificationService.logException(exception, "Can't load auth model"); - } - } - - private async loadDrivers() { - try { - await this.dbDriverResource.load(CachedMapAllKey); - } catch (exception: any) { - this.notificationService.logException(exception, "Can't load database drivers", '', true); - } - } -} diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx deleted file mode 100644 index f37aec66cf..0000000000 --- a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx +++ /dev/null @@ -1,51 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { Button, useTranslate } from '@cloudbeaver/core-blocks'; - -const styles = css` - footer-container { - display: flex; - width: min-content; - flex: 1; - align-items: center; - justify-content: flex-end; - } - footer-container > :not(:first-child) { - margin-left: 16px; - } - Button { - flex: 0 0 auto; - } -`; - -export interface Props { - isAuthenticating: boolean; - onLogin: () => void; - className?: string; -} - -export const DBAuthDialogFooter = observer>(function DBAuthDialogFooter({ - isAuthenticating, - onLogin, - className, - children, -}) { - const translate = useTranslate(); - - return styled(styles)( - - {children} - - , - ); -}); diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.m.css b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.m.css deleted file mode 100644 index 7fb2a797bf..0000000000 --- a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.m.css +++ /dev/null @@ -1,14 +0,0 @@ -.submittingForm { - overflow: auto; - margin: auto; - flex: 1; - display: flex; - flex-direction: column; -} -.connectionAuthenticationFormLoader { - align-content: center; -} -.errorMessage { - composes: theme-background-secondary theme-text-on-secondary from global; - flex: 1; -} diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx index 00a6c23d63..ff4066e987 100644 --- a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx @@ -1,33 +1,18 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { - CommonDialogBody, - CommonDialogFooter, - CommonDialogHeader, - CommonDialogWrapper, - ErrorMessage, - Form, - Loader, - s, - useAdministrationSettings, - useFocus, - useS, -} from '@cloudbeaver/core-blocks'; -import { IConnectionInfoParams, useConnectionInfo, useDBDriver } from '@cloudbeaver/core-connections'; -import { useController } from '@cloudbeaver/core-di'; +import { CommonDialogHeader, CommonDialogWrapper, useResource } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, ConnectionPublicSecretsResource, DBDriverResource, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; import type { DialogComponent } from '@cloudbeaver/core-dialogs'; -import { ConnectionAuthenticationFormLoader } from '../ConnectionAuthentication/ConnectionAuthenticationFormLoader'; -import style from './DatabaseAuthDialog.m.css'; -import { DBAuthDialogController } from './DBAuthDialogController'; -import { DBAuthDialogFooter } from './DBAuthDialogFooter'; +import { DatabaseCredentialsAuthDialog } from './DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.js'; +import { DatabaseSecretAuthDialog } from './DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.js'; interface Payload { connection: IConnectionInfoParams; @@ -36,59 +21,31 @@ interface Payload { } export const DatabaseAuthDialog: DialogComponent = observer(function DatabaseAuthDialog({ payload, options, rejectDialog, resolveDialog }) { - const connection = useConnectionInfo(payload.connection); - const controller = useController(DBAuthDialogController, payload.connection, payload.networkHandlers, resolveDialog); - const styles = useS(style); + const connectionInfoLoader = useResource(DatabaseAuthDialog, ConnectionInfoResource, payload.connection); + const driverLoader = useResource(DatabaseAuthDialog, DBDriverResource, connectionInfoLoader.data?.driverId || null); + const connectionPublicSecretsLoader = useResource(DatabaseSecretAuthDialog, ConnectionPublicSecretsResource, payload.connection); + const useSharedCredentials = (connectionPublicSecretsLoader.data?.length || 0) > 1; - const { driver } = useDBDriver(connection.connectionInfo?.driverId || ''); - const { credentialsSavingEnabled } = useAdministrationSettings(); - const [focusedRef] = useFocus({ focusFirstChild: true }); - - let authModelId: string | null = null; - - if (connection.connectionInfo?.authNeeded || payload.resetCredentials) { - authModelId = connection.connectionInfo?.authModel || driver?.defaultAuthModel || null; - } + const subtitle = connectionInfoLoader.data?.name; return ( - + - -
- {!connection.isLoaded() || connection.isLoading() || !controller.configured ? ( - - ) : ( - - )} - -
- - - {controller.error.responseMessage && ( - - )} - - + {useSharedCredentials ? ( + + ) : ( + + )}
); }); diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.module.css b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.module.css new file mode 100644 index 0000000000..405a6b0724 --- /dev/null +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.module.css @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.submittingForm { + overflow: auto; + margin: auto; + flex: 1; + display: flex; + flex-direction: column; +} +.connectionAuthenticationFormLoader { + align-content: center; +} +.errorMessage { + composes: theme-background-secondary theme-text-on-secondary from global; + flex: 1; +} diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.tsx new file mode 100644 index 0000000000..23be7d7428 --- /dev/null +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.tsx @@ -0,0 +1,88 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { + CommonDialogBody, + CommonDialogFooter, + ErrorMessage, + Form, + Loader, + s, + useAdministrationSettings, + useAutoLoad, + useErrorDetails, + useFocus, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import type { IConnectionInfoParams } from '@cloudbeaver/core-connections'; + +import { ConnectionAuthenticationFormLoader } from '../../ConnectionAuthentication/ConnectionAuthenticationFormLoader.js'; +import style from './DatabaseCredentialsAuthDialog.module.css'; +import { DatabaseCredentialsAuthDialogFooter } from './DatabaseCredentialsAuthDialogFooter.js'; +import { useDatabaseCredentialsAuthDialog } from './useDatabaseCredentialsAuthDialog.js'; + +interface Props { + connection: IConnectionInfoParams; + networkHandlers: string[]; + resetCredentials?: boolean; + onLogin?: () => void; +} + +export const DatabaseCredentialsAuthDialog = observer(function DatabaseCredentialsAuthDialog({ + connection, + networkHandlers, + resetCredentials, + onLogin, +}) { + const styles = useS(style); + const translate = useTranslate(); + const [focusedRef] = useFocus({ focusFirstChild: true }); + + const { credentialsSavingEnabled } = useAdministrationSettings(); + const dialog = useDatabaseCredentialsAuthDialog(connection, networkHandlers, resetCredentials, onLogin); + const errorDetails = useErrorDetails(dialog.authException); + + useAutoLoad(DatabaseCredentialsAuthDialog, dialog); + + return ( + <> + +
+ + + +
+
+ + + {dialog.authException && ( + + )} + + + + ); +}); diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.module.css b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.module.css new file mode 100644 index 0000000000..41150d4255 --- /dev/null +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.module.css @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.footerContainer { + display: flex; + width: min-content; + flex: 1; + align-items: center; + justify-content: flex-end; +} + +.footerContainer > :not(:first-child) { + margin-left: 16px; +} + +.button { + flex: 0 0 auto; +} diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.tsx new file mode 100644 index 0000000000..40592aff68 --- /dev/null +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.tsx @@ -0,0 +1,37 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Button, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; + +import styles from './DatabaseCredentialsAuthDialogFooter.module.css'; + +export interface Props { + isAuthenticating: boolean; + onLogin: () => void; + className?: string; +} + +export const DatabaseCredentialsAuthDialogFooter = observer>(function DatabaseCredentialsAuthDialogFooter({ + isAuthenticating, + onLogin, + className, + children, +}) { + const style = useS(styles); + const translate = useTranslate(); + + return ( +
+ {children} + +
+ ); +}); diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/useDatabaseCredentialsAuthDialog.ts b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/useDatabaseCredentialsAuthDialog.ts new file mode 100644 index 0000000000..5483daf723 --- /dev/null +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/useDatabaseCredentialsAuthDialog.ts @@ -0,0 +1,197 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { action, computed, observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import { + type ConnectionInfoAuthProperties, + ConnectionInfoAuthPropertiesResource, + ConnectionInfoNetworkHandlersResource, + ConnectionInfoResource, + type ConnectionInitConfig, + type DBDriver, + DBDriverResource, + type IConnectionInfoParams, + USER_NAME_PROPERTY_ID, +} from '@cloudbeaver/core-connections'; +import { useService } from '@cloudbeaver/core-di'; +import { NetworkHandlerAuthType } from '@cloudbeaver/core-sdk'; +import type { ILoadableState } from '@cloudbeaver/core-utils'; + +import type { IConnectionAuthenticationConfig } from '../../ConnectionAuthentication/IConnectionAuthenticationConfig.js'; + +interface IState extends ILoadableState { + readonly authModelId: string | null; + driver: DBDriver | null; + connectionAuthProperties: ConnectionInfoAuthProperties | null; + config: IConnectionAuthenticationConfig; + authenticating: boolean; + loading: boolean; + loaded: boolean; + exception: Error | null; + authException: Error | null; + load: () => Promise; + login: () => Promise; + getConfig: () => ConnectionInitConfig; +} + +export function useDatabaseCredentialsAuthDialog( + key: IConnectionInfoParams, + networkHandlers: string[], + resetCredentials?: boolean, + onInit?: () => void, +) { + const connectionInfoResource = useService(ConnectionInfoResource); + const dbDriverResource = useService(DBDriverResource); + const connectionInfoAuthPropertiesResource = useService(ConnectionInfoAuthPropertiesResource); + const connectionInfoNetworkHandlersLoader = useService(ConnectionInfoNetworkHandlersResource); + + const state: IState = useObservableRef( + () => ({ + get authModelId() { + if (!this.connectionAuthProperties?.authNeeded && !this.resetCredentials) { + return null; + } + + return this.connectionAuthProperties?.authModel || this.driver?.defaultAuthModel || null; + }, + connectionAuthProperties: null as ConnectionInfoAuthProperties | null, + driver: null as DBDriver | null, + authenticating: false, + loading: false, + loaded: false, + exception: null, + authException: null, + config: { + credentials: {}, + networkHandlersConfig: [], + saveCredentials: false, + } as IConnectionAuthenticationConfig, + async load() { + if (this.loaded || this.loading) { + return; + } + + try { + this.exception = null; + this.loading = true; + + const [connectionAuthProperties, connection, connectionNetworkHandlersConfig] = await Promise.all([ + this.connectionInfoAuthPropertiesResource.load(this.key), + this.connectionInfoResource.load(this.key), + this.connectionInfoNetworkHandlersLoader.load(this.key), + ]); + + const driver = await this.dbDriverResource.load(connection.driverId); + + if (connectionAuthProperties.authNeeded) { + const property = connectionAuthProperties?.authProperties.find(property => property.id === USER_NAME_PROPERTY_ID); + + if (property?.value) { + this.config.credentials[USER_NAME_PROPERTY_ID] = property.value; + } + } + + for (const id of this.networkHandlers) { + const handler = connectionNetworkHandlersConfig.networkHandlersConfig?.find(handler => handler.id === id); + + if (handler && (handler.userName || handler.authType !== NetworkHandlerAuthType.Password)) { + this.config.networkHandlersConfig.push({ + id: handler.id, + authType: handler.authType, + userName: handler.userName, + password: handler.password, + savePassword: handler.savePassword, + }); + } + } + + this.config.saveCredentials = connectionAuthProperties.saveCredentials; + this.connectionAuthProperties = connectionAuthProperties; + this.driver = driver; + + this.loaded = true; + } catch (exception: any) { + this.exception = exception; + } finally { + this.loading = false; + } + }, + async login() { + if (this.authenticating) { + return; + } + + try { + this.authException = null; + this.authenticating = true; + await this.connectionInfoResource.init(this.getConfig()); + this.onInit?.(); + } catch (exception: any) { + this.authException = exception; + } finally { + this.authenticating = false; + } + }, + getConfig() { + const config: ConnectionInitConfig = { + projectId: this.key.projectId, + connectionId: this.key.connectionId, + }; + + if (this.authModelId) { + if (Object.keys(this.config.credentials).length > 0) { + config.credentials = this.config.credentials; + } + + config.saveCredentials = this.config.saveCredentials; + } + + if (this.config.networkHandlersConfig.length > 0) { + config.networkCredentials = this.config.networkHandlersConfig; + } + + return config; + }, + isLoading() { + return this.loading; + }, + isLoaded() { + return this.loaded; + }, + isError() { + return !!this.exception; + }, + }), + { + authModelId: computed, + config: observable, + connectionAuthProperties: observable.ref, + driver: observable.ref, + authenticating: observable.ref, + loading: observable.ref, + loaded: observable.ref, + exception: observable.ref, + authException: observable.ref, + load: action.bound, + login: action.bound, + }, + { + connectionInfoResource, + dbDriverResource, + key, + networkHandlers, + resetCredentials, + connectionInfoAuthPropertiesResource, + connectionInfoNetworkHandlersLoader, + onInit, + }, + ); + + return state; +} diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.module.css b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.module.css new file mode 100644 index 0000000000..27fcdd50a3 --- /dev/null +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.infoMessage { + white-space: pre-line; +} diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.tsx new file mode 100644 index 0000000000..2fd9113578 --- /dev/null +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.tsx @@ -0,0 +1,95 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; + +import { + CommonDialogBody, + CommonDialogFooter, + ExceptionMessage, + Group, + ItemList, + ListItem, + ListItemName, + Loader, + s, + useObservableRef, + useResource, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, ConnectionPublicSecretsResource, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; + +import style from './DatabaseSecretAuthDialog.module.css'; + +interface Props { + connectionKey: IConnectionInfoParams; + onLogin?: () => void; +} + +export const DatabaseSecretAuthDialog = observer(function DatabaseSecretAuthDialog({ connectionKey, onLogin }) { + const styles = useS(style); + const translate = useTranslate(); + const connectionInfoLoader = useResource(DatabaseSecretAuthDialog, ConnectionInfoResource, connectionKey); + const state = useObservableRef( + () => ({ + exception: null as Error | null, + authenticating: false, + }), + { + exception: observable.ref, + authenticating: observable.ref, + }, + false, + ); + const connectionPublicSecretsLoader = useResource(DatabaseSecretAuthDialog, ConnectionPublicSecretsResource, connectionKey); + const secrets = connectionPublicSecretsLoader.data || []; + + async function handleSecretSelect(secretId: string) { + try { + state.authenticating = true; + await connectionInfoLoader.resource.init({ + ...connectionKey, + selectedSecretId: secretId, + }); + state.exception = null; + onLogin?.(); + } catch (exception: any) { + state.exception = exception; + } finally { + state.authenticating = false; + } + } + + if (state.authenticating) { + return ; + } + + return ( + <> + + + {secrets.map(secret => ( + handleSecretSelect(secret.secretId)}> + {secret.displayName} + + ))} + + + + + {state.exception ? ( + + ) : ( + {translate('plugin_connections_connection_auth_secret_description')} + )} + + + + ); +}); diff --git a/webapp/packages/plugin-connections/src/LocaleService.ts b/webapp/packages/plugin-connections/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-connections/src/LocaleService.ts +++ b/webapp/packages/plugin-connections/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index 67b3bbc04c..c9231e1e7e 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -1,12 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { untracked } from 'mobx'; - import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { ConfirmationDialogDelete } from '@cloudbeaver/core-blocks'; import { @@ -14,26 +12,28 @@ import { ConnectionFolderProjectKey, ConnectionFolderResource, ConnectionInfoResource, - ConnectionsManagerService, createConnectionFolderParam, createConnectionParam, getConnectionFolderId, getConnectionFolderIdFromNodeId, - IConnectionFolderParam, - IConnectionInfoParams, + type IConnectionFolderParam, + type IConnectionInfoParams, + isConnectionNode, } from '@cloudbeaver/core-connections'; import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { + DATA_CONTEXT_NAV_NODE, ENodeMoveType, getNodesFromContext, - INodeMoveData, - NAV_NODE_TYPE_FOLDER, - NavNode, + type INodeMoveData, + isConnectionFolder, + isProjectNode, + type NavNode, NavNodeInfoResource, NavNodeManagerService, navNodeMoveContext, @@ -42,24 +42,43 @@ import { ProjectsNavNodeService, ROOT_NODE_PATH, } from '@cloudbeaver/core-navigation-tree'; -import { getProjectNodeId, NAV_NODE_TYPE_PROJECT, ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey, ResourceKeyAlias, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { getProjectNodeId, ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { + CachedMapAllKey, + getCachedMapResourceLoaderState, + resourceKeyList, + type ResourceKeySimple, + ResourceKeyUtils, +} from '@cloudbeaver/core-resource'; import { createPath } from '@cloudbeaver/core-utils'; -import { ACTION_NEW_FOLDER, ActionService, DATA_CONTEXT_MENU, IAction, MenuService } from '@cloudbeaver/core-view'; -import { DATA_CONTEXT_ELEMENTS_TREE, type IElementsTree, MENU_ELEMENTS_TREE_TOOLS } from '@cloudbeaver/plugin-navigation-tree'; +import { ACTION_NEW_FOLDER, ActionService, type IAction, MenuService } from '@cloudbeaver/core-view'; +import { + DATA_CONTEXT_ELEMENTS_TREE, + MENU_ELEMENTS_TREE_TOOLS, + NavigationTreeService, + TreeSelectionService, +} from '@cloudbeaver/plugin-navigation-tree'; import { FolderDialog } from '@cloudbeaver/plugin-projects'; -import { NAV_NODE_TYPE_CONNECTION } from './NAV_NODE_TYPE_CONNECTION'; - -interface ITargetNode { - projectId: string; - folderId?: string; - - projectNodeId: string; - selectProject: boolean; -} +import { ACTION_TREE_CREATE_FOLDER } from '../Actions/ACTION_TREE_CREATE_FOLDER.js'; -@injectable() +@injectable(() => [ + LocalizationService, + UserInfoResource, + NavTreeResource, + ActionService, + MenuService, + ConnectionInfoResource, + NavNodeManagerService, + ConnectionFolderResource, + CommonDialogService, + NotificationService, + NavNodeInfoResource, + ProjectInfoResource, + ProjectsNavNodeService, + NavigationTreeService, + TreeSelectionService, +]) export class ConnectionFoldersBootstrap extends Bootstrap { constructor( private readonly localizationService: LocalizationService, @@ -70,17 +89,18 @@ export class ConnectionFoldersBootstrap extends Bootstrap { private readonly connectionInfoResource: ConnectionInfoResource, private readonly navNodeManagerService: NavNodeManagerService, private readonly connectionFolderResource: ConnectionFolderResource, - private readonly connectionsManagerService: ConnectionsManagerService, private readonly commonDialogService: CommonDialogService, private readonly notificationService: NotificationService, private readonly navNodeInfoResource: NavNodeInfoResource, private readonly projectInfoResource: ProjectInfoResource, private readonly projectsNavNodeService: ProjectsNavNodeService, + private readonly navigationTreeService: NavigationTreeService, + private readonly treeSelectionService: TreeSelectionService, ) { super(); } - register(): void | Promise { + override register(): void { this.navNodeInfoResource.onItemUpdate.addHandler(this.syncWithNavTree.bind(this)); this.navNodeInfoResource.onItemDelete.addHandler(this.syncWithNavTree.bind(this)); this.navNodeManagerService.onMove.addHandler(this.moveConnectionToFolder.bind(this)); @@ -109,7 +129,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap { const result = await this.commonDialogService.open(ConfirmationDialogDelete, { title: 'ui_data_delete_confirmation', - message: this.localizationService.translate('connections_public_connection_folder_delete_confirmation', undefined, { name: nodes }), + message: this.localizationService.translate('plugin_connections_connection_folder_delete_confirmation', undefined, { name: nodes }), confirmActionText: 'ui_delete', }); @@ -122,37 +142,30 @@ export class ConnectionFoldersBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'tree-tools-menu-folders-handler', + contexts: [DATA_CONTEXT_ELEMENTS_TREE], isActionApplicable: (context, action) => { - const tree = context.tryGet(DATA_CONTEXT_ELEMENTS_TREE); + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; - if (action !== ACTION_NEW_FOLDER || !tree || !this.userInfoResource.data || tree.baseRoot !== ROOT_NODE_PATH) { + if (action !== ACTION_NEW_FOLDER || !this.userInfoResource.isAuthenticated() || tree.baseRoot !== ROOT_NODE_PATH) { return false; } - const targetNode = this.getTargetNode(tree); + const targetNode = this.treeSelectionService.getFirstSelectedNode( + tree, + getProjectNodeId, + project => project.canEditDataSources, + isProjectNode, + isConnectionFolder, + ); return targetNode !== undefined; }, - // isDisabled: (context, action) => { - // const tree = context.tryGet(DATA_CONTEXT_ELEMENTS_TREE); - - // if (!tree) { - // return true; - // } - - // if (action === ACTION_NEW_FOLDER) { - // const targetNode = this.getTargetNode(tree); - - // return targetNode === undefined; - // } - - // return false; - // }, + getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), handler: this.elementsTreeActionHandler.bind(this), }); this.menuService.addCreator({ - isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_ELEMENTS_TREE_TOOLS, + menus: [MENU_ELEMENTS_TREE_TOOLS], getItems: (context, items) => { if (!items.includes(ACTION_NEW_FOLDER)) { return [...items, ACTION_NEW_FOLDER]; @@ -161,11 +174,47 @@ export class ConnectionFoldersBootstrap extends Bootstrap { return items; }, }); + + this.menuService.addCreator({ + root: true, + contexts: [DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_ELEMENTS_TREE], + isApplicable: context => { + const node = context.get(DATA_CONTEXT_NAV_NODE)!; + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; + const targetNode = this.treeSelectionService.getFirstSelectedNode( + tree, + getProjectNodeId, + project => project.canEditDataSources, + isProjectNode, + isConnectionFolder, + ); + + if ( + ![isConnectionFolder, isProjectNode].some(check => check(node)) || + !this.userInfoResource.isAuthenticated() || + tree.baseRoot !== ROOT_NODE_PATH || + targetNode === undefined + ) { + return false; + } + + return true; + }, + getItems: (context, items) => [...items, ACTION_TREE_CREATE_FOLDER], + }); + + this.actionService.addHandler({ + id: 'nav-tree-create-create-folders-handler', + // menus: [MENU_NAVIGATION_TREE_CREATE], + contexts: [DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_ELEMENTS_TREE], + actions: [ACTION_TREE_CREATE_FOLDER], + getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), + handler: this.elementsTreeActionHandler.bind(this), + }); } - load(): void | Promise {} private async moveConnectionToFolder({ type, targetNode, moveContexts }: INodeMoveData, contexts: IExecutionContextProvider) { - if (![NAV_NODE_TYPE_PROJECT, NAV_NODE_TYPE_FOLDER].includes(targetNode.nodeType!)) { + if (![isProjectNode, isConnectionFolder].some(check => check(targetNode))) { return; } @@ -179,7 +228,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap { const supported = nodes.every(node => { if ( - ![NAV_NODE_TYPE_CONNECTION, NAV_NODE_TYPE_FOLDER, NAV_NODE_TYPE_PROJECT].includes(node.nodeType!) || + ![isConnectionNode, isConnectionFolder, isProjectNode].some(check => check(node)) || targetProject !== this.projectsNavNodeService.getProject(node.id) || children.includes(node.id) || targetNode.id === node.id @@ -202,15 +251,15 @@ export class ConnectionFoldersBootstrap extends Bootstrap { const childrenNode = this.navNodeInfoResource.get(resourceKeyList(children)); const folderDuplicates = nodes.filter( node => - node.nodeType === NAV_NODE_TYPE_FOLDER && - (childrenNode.some(child => child?.nodeType === NAV_NODE_TYPE_FOLDER && child.name === node.name) || - nodes.some(child => child.nodeType === NAV_NODE_TYPE_FOLDER && child.name === node.name && child.id !== node.id)), + isConnectionFolder(node) && + (childrenNode.some(child => child && isConnectionFolder(child) && child.name === node.name) || + nodes.some(child => isConnectionFolder(child) && child.name === node.name && child.id !== node.id)), ); if (folderDuplicates.length > 0) { this.notificationService.logError({ - title: 'connections_public_connection_folder_move_failed', - message: this.localizationService.translate('connections_public_connection_folder_move_duplication', undefined, { + title: 'plugin_connections_connection_folder_move_failed', + message: this.localizationService.translate('plugin_connections_connection_folder_move_duplication', undefined, { name: folderDuplicates.map(node => `"${node.name}"`).join(', '), }), }); @@ -233,7 +282,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap { this.connectionInfoResource.markOutdated(resourceKeyList(connections)); } catch (exception: any) { - this.notificationService.logException(exception, 'connections_public_connection_folder_move_failed'); + this.notificationService.logException(exception, 'plugin_connections_connection_folder_move_failed'); } } } @@ -246,8 +295,15 @@ export class ConnectionFoldersBootstrap extends Bootstrap { } switch (action) { + case ACTION_TREE_CREATE_FOLDER: case ACTION_NEW_FOLDER: { - const targetNode = this.getTargetNode(tree); + const targetNode = this.treeSelectionService.getFirstSelectedNode( + tree, + getProjectNodeId, + project => project.canEditDataSources, + isProjectNode, + isConnectionFolder, + ); if (!targetNode) { this.notificationService.logError({ title: "Can't create folder", message: 'core_projects_no_default_project' }); @@ -261,11 +317,11 @@ export class ConnectionFoldersBootstrap extends Bootstrap { } const result = await this.commonDialogService.open(FolderDialog, { - value: this.localizationService.translate('ui_folder_new'), + value: this.localizationService.translate('ui_folder_new_default_name'), projectId: targetNode.projectId, folder: parentFolderParam?.folderId, title: 'core_view_action_new_folder', - icon: '/icons/folder.svg#root', + icon: '/icons/folder.svg', create: true, selectProject: targetNode.selectProject, validation: async ({ name, folder, projectId }, setMessage) => { @@ -276,21 +332,8 @@ export class ConnectionFoldersBootstrap extends Bootstrap { return false; } - let parentKey: - | ResourceKeyAlias< - any, - { - projectId: string; - } - > - | IConnectionFolderParam = ConnectionFolderProjectKey(projectId); - - if (folder) { - parentKey = createConnectionFolderParam(projectId, folder); - } - try { - await this.connectionFolderResource.load(parentKey); + await this.connectionFolderResource.load(ConnectionFolderProjectKey(projectId)); return !this.connectionFolderResource.has(createConnectionFolderParam(projectId, createPath(folder, trimmed))); } catch (exception: any) { @@ -308,6 +351,10 @@ export class ConnectionFoldersBootstrap extends Bootstrap { ? getConnectionFolderId(createConnectionFolderParam(result.projectId, result.folder)) : getProjectNodeId(result.projectId), ); + + const newFolderId = getConnectionFolderId(createConnectionFolderParam(result.projectId, createPath(result.folder, result.name))); + await this.navNodeInfoResource.loadNodeParents(newFolderId); + await this.navigationTreeService.showNode(newFolderId, this.navNodeInfoResource.getParents(newFolderId)); } catch (exception: any) { this.notificationService.logException(exception, "Can't create folder"); } @@ -318,58 +365,11 @@ export class ConnectionFoldersBootstrap extends Bootstrap { } } - private async syncWithNavTree(key: ResourceKeySimple) { + private syncWithNavTree(key: ResourceKeySimple) { const isFolder = ResourceKeyUtils.some(key, nodeId => this.connectionFolderResource.fromNodeId(nodeId) !== undefined); if (isFolder) { this.connectionFolderResource.markOutdated(); } } - - private getTargetNode(tree: IElementsTree): ITargetNode | undefined { - untracked(() => this.projectInfoResource.load(CachedMapAllKey)); - const selected = tree.getSelected(); - - if (selected.length === 0) { - const editableProjects = this.connectionsManagerService.createConnectionProjects; - - if (editableProjects.length > 0) { - const project = editableProjects[0]; - - return { - projectId: project.id, - projectNodeId: getProjectNodeId(project.id), - selectProject: editableProjects.length > 1, - }; - } - return; - } - - const targetFolder = selected[0]; - const parentIds = [...this.navNodeInfoResource.getParents(targetFolder), targetFolder]; - const parents = this.navNodeInfoResource.get(resourceKeyList(parentIds)); - const projectNode = parents.find(parent => parent?.nodeType === NAV_NODE_TYPE_PROJECT); - - if (!projectNode) { - return; - } - - const project = this.projectsNavNodeService.getByNodeId(projectNode.id); - - if (!project?.canEditDataSources) { - return; - } - - const targetFolderNode = parents - .slice() - .reverse() - .find(parent => parent?.nodeType === NAV_NODE_TYPE_FOLDER); - - return { - projectId: project.id, - folderId: targetFolderNode?.id, - projectNodeId: projectNode.id, - selectProject: false, - }; - } } diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionNavNodeService.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionNavNodeService.ts new file mode 100644 index 0000000000..f3300dc39d --- /dev/null +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionNavNodeService.ts @@ -0,0 +1,319 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { action, makeObservable, runInAction } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { ExecutorInterrupter, type IAsyncContextLoader, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { + type INodeNavigationData, + NavNodeInfoResource, + NavNodeManagerService, + NavTreeResource, + NodeManagerUtils, +} from '@cloudbeaver/core-navigation-tree'; +import { getProjectNodeId } from '@cloudbeaver/core-projects'; +import { isResourceAlias, type ResourceKey, resourceKeyList, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { ServerEventId } from '@cloudbeaver/core-root'; +import { + ConnectionInfoResource, + ContainerResource, + ConnectionsManagerService, + getFolderNodeParents, + type Connection, + ConnectionInfoActiveProjectKey, + type IConnectionInfoParams, + getConnectionParentId, + createConnectionParam, + ConnectionFolderEventHandler, + type IConnectionFolderEvent, +} from '@cloudbeaver/core-connections'; +import { NavigationTreeService } from '@cloudbeaver/plugin-navigation-tree'; + +@injectable(() => [ + ConnectionInfoResource, + NavTreeResource, + ContainerResource, + NavNodeInfoResource, + NavNodeManagerService, + NavigationTreeService, + ConnectionsManagerService, + ConnectionFolderEventHandler, +]) +export class ConnectionNavNodeService { + constructor( + private readonly connectionInfoResource: ConnectionInfoResource, + private readonly navTreeResource: NavTreeResource, + private readonly containerResource: ContainerResource, + private readonly navNodeInfoResource: NavNodeInfoResource, + private readonly navNodeManagerService: NavNodeManagerService, + private readonly navigationTreeService: NavigationTreeService, + private readonly connectionsManagerService: ConnectionsManagerService, + private readonly connectionFolderEventHandler: ConnectionFolderEventHandler, + ) { + makeObservable(this, { + connectionUpdateHandler: action.bound, + connectionRemoveHandler: action.bound, + connectionCreateHandler: action.bound, + }); + + this.connectionInfoResource.onDataOutdated.addHandler(this.connectionUpdateHandler); // duplicates onItemAdd in some cases + this.connectionInfoResource.onItemUpdate.addHandler(this.connectionUpdateHandler); + this.connectionInfoResource.onItemDelete.addHandler(this.connectionRemoveHandler); + this.connectionInfoResource.onConnectionCreate.addHandler(this.connectionCreateHandler); + this.navNodeInfoResource.onDataOutdated.addHandler(this.navNodeOutdateHandler.bind(this)); + + this.navTreeResource.before(this.preloadConnectionInfo.bind(this)); + + this.navNodeManagerService.navigator.addHandler(this.navigateHandler.bind(this)); + + this.connectionInfoResource.connect(this.navTreeResource); + + this.connectionFolderEventHandler.onEvent( + ServerEventId.CbDatasourceFolderCreated, + data => { + const parents = data.nodePaths.map(nodeId => { + const parents = getFolderNodeParents(nodeId); + + return parents[parents.length - 1]!; + }); + this.navTreeResource.markOutdated(resourceKeyList(parents)); + }, + undefined, + this.navTreeResource, + ); + this.connectionFolderEventHandler.onEvent( + ServerEventId.CbDatasourceFolderDeleted, + data => { + const parents = data.nodePaths.map(nodeId => { + const parents = getFolderNodeParents(nodeId); + + return parents[parents.length - 1]!; + }); + + this.navTreeResource.deleteInNode( + resourceKeyList(parents), + data.nodePaths.map(value => [value]), + ); + }, + undefined, + this.navTreeResource, + ); + this.connectionFolderEventHandler.onEvent( + ServerEventId.CbDatasourceFolderUpdated, + data => { + this.navTreeResource.markOutdated(resourceKeyList(data.nodePaths)); + }, + undefined, + this.navTreeResource, + ); + } + + navigationNavNodeConnectionContext: IAsyncContextLoader = async (context, { nodeId }) => { + await this.connectionInfoResource.load(ConnectionInfoActiveProjectKey); + const connection = this.connectionInfoResource.getConnectionForNode(nodeId); + + return connection; + }; + + private async preloadConnectionInfo(key: ResourceKey, context: IExecutionContextProvider>) { + if (isResourceAlias(key)) { + return; + } + if (!ResourceKeyUtils.some(key, key => NodeManagerUtils.isDatabaseObject(key))) { + return; + } + + await this.connectionInfoResource.load(ConnectionInfoActiveProjectKey); + + const notConnected = ResourceKeyUtils.some(key, key => { + const connection = this.connectionInfoResource.getConnectionForNode(key); + + return !!connection && !connection.connected; + }); + + if (notConnected) { + ExecutorInterrupter.interrupt(context); + throw new Error('Connection not established'); + } + } + + private connectionUpdateHandler(key: ResourceKey) { + runInAction(() => { + let connectionInfos = this.connectionInfoResource.get(key); + const outdatedTrees: string[] = []; + const closedConnections: string[] = []; + + connectionInfos = Array.isArray(connectionInfos) ? connectionInfos : [connectionInfos]; + for (const connectionInfo of connectionInfos) { + if (!connectionInfo?.nodePath) { + return; + } + + const node = this.navNodeInfoResource.get(connectionInfo.nodePath); + const parentId = getConnectionParentId(connectionInfo.projectId, connectionInfo.folder); // new parent + + if (!connectionInfo.connected) { + closedConnections.push(connectionInfo.nodePath); + outdatedTrees.push(connectionInfo.nodePath); + } + + const folderId = node?.parentId; // current parent + + if (folderId && !outdatedTrees.includes(folderId)) { + outdatedTrees.push(folderId); + } + + if (!outdatedTrees.includes(parentId)) { + outdatedTrees.push(parentId); + } + } + + if (closedConnections.length > 0) { + const key = resourceKeyList(closedConnections); + + if (this.navTreeResource.has(key)) { + this.navTreeResource.delete(key); + } + } + + if (outdatedTrees.length > 0) { + const key = resourceKeyList(outdatedTrees); + this.navTreeResource.markOutdated(key); + } + }); + } + + private connectionRemoveHandler(key: ResourceKeySimple) { + runInAction(() => { + ResourceKeyUtils.forEach(key, key => { + const connectionInfo = this.connectionInfoResource.get(key); + + if (!connectionInfo) { + return; + } + + const nodePath = connectionInfo.nodePath ?? NodeManagerUtils.connectionIdToConnectionNodeId(key.connectionId); + + const node = this.navNodeInfoResource.get(nodePath); + const folder = node?.parentId ?? getProjectNodeId(key.projectId); + const parents = this.navNodeInfoResource.getParents(folder); + + for (let i = parents.length - 1; i >= 0; i--) { + const parent = parents[i]!; + const children = this.navTreeResource.get(parent) ?? []; + + if (children.length > 1) { + this.navTreeResource.markOutdated(parent); + break; + } + } + + if (nodePath) { + this.navTreeResource.deleteInNode(folder, [nodePath]); + } + }); + }); + } + + private async connectionCreateHandler(connection: Connection) { + if (!connection.nodePath) { + return; + } + + try { + const parentId = getConnectionParentId(connection.projectId, connection.folder); + + await this.navTreeResource.waitLoad(); + + if (!this.navTreeResource.has(parentId)) { + await this.navNodeInfoResource.loadNodeParents(parentId); + const parents = this.navNodeInfoResource.getParents(parentId); + + // allows to preload missing folders + const loadingPath = [...parents, parentId]; + for (let i = 0; i < loadingPath.length - 1; i++) { + const parent = loadingPath[i]!; + const nextParent = loadingPath[i + 1]!; + const children = this.navTreeResource.get(parent) ?? []; + + if (!children.includes(nextParent)) { + this.navTreeResource.markOutdated(parent); + break; + } + } + + const preloaded = await this.navTreeResource.preloadNodeParents(parents, parentId); + + if (!preloaded) { + return; + } + } + + let children = this.navTreeResource.get(parentId); + + if (!children || children.includes(connection.nodePath)) { + return; + } + + const connectionNode = await this.navNodeInfoResource.load(connection.nodePath); + await this.navTreeResource.waitLoad(); + + this.navNodeInfoResource.setParent(connection.nodePath, parentId); + + children = this.navTreeResource.get(parentId); + + if (!children || children.includes(connection.nodePath)) { + return; // double check + } + + let insertIndex = 0; + + const nodes = this.navNodeInfoResource.get(resourceKeyList(children)); + + for (const node of nodes) { + if (!node?.folder && node?.name?.localeCompare(connectionNode.name!) === 1) { + break; + } + insertIndex++; + } + + this.navTreeResource.insertToNode(parentId, insertIndex, connection.nodePath); + } finally { + await this.navNodeInfoResource.loadNodeParents(connection.nodePath); + await this.navigationTreeService.showNode(connection.nodePath, this.navNodeInfoResource.getParents(connection.nodePath)); + } + } + + private async navigateHandler({ nodeId }: INodeNavigationData, contexts: IExecutionContextProvider): Promise { + let connection: Connection | undefined | null = await contexts.getContext(this.navigationNavNodeConnectionContext); + + if (NodeManagerUtils.isDatabaseObject(nodeId) && connection) { + connection = await this.connectionsManagerService.requireConnection(createConnectionParam(connection)); + + if (!connection?.connected) { + ExecutorInterrupter.interrupt(contexts); + } + } + } + + private navNodeOutdateHandler(key: ResourceKey) { + const outdateKeys = this.containerResource.entries + .filter(([_, value]) => + ResourceKeyUtils.some( + key, + key => + value.parentNode?.id === key || + value.catalogList.some(catalog => catalog.catalog?.id === key || catalog.schemaList.some(schema => schema?.id === key)) || + value.schemaList.some(schema => schema?.id === key), + ), + ) + .map(([key]) => key); + + this.containerResource.markOutdated(resourceKeyList(outdateKeys)); + } +} diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionsExplorerBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionsExplorerBootstrap.ts new file mode 100644 index 0000000000..e10a38652b --- /dev/null +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionsExplorerBootstrap.ts @@ -0,0 +1,37 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree'; +import { MenuService } from '@cloudbeaver/core-view'; +import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS } from '@cloudbeaver/plugin-navigation-tree'; +import { MENU_TREE_CREATE_CONNECTION } from '../Actions/MENU_TREE_CREATE_CONNECTION.js'; + +@injectable(() => [MenuService]) +export class ConnectionsExplorerBootstrap extends Bootstrap { + constructor(private readonly menuService: MenuService) { + super(); + } + + override register(): void { + this.menuService.addCreator({ + menus: [MENU_ELEMENTS_TREE_TOOLS], + isApplicable: context => { + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; + + return tree.baseRoot === ROOT_NODE_PATH; + }, + getItems: (context, items) => { + if (!items.includes(MENU_TREE_CREATE_CONNECTION)) { + return [...items, MENU_TREE_CREATE_CONNECTION]; + } + + return items; + }, + }); + } +} diff --git a/webapp/packages/plugin-connections/src/NavNodes/NAV_NODE_TYPE_CONNECTION.ts b/webapp/packages/plugin-connections/src/NavNodes/NAV_NODE_TYPE_CONNECTION.ts index 88a7ac870e..67894888c7 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/NAV_NODE_TYPE_CONNECTION.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/NAV_NODE_TYPE_CONNECTION.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-connections/src/PluginBootstrap.ts b/webapp/packages/plugin-connections/src/PluginBootstrap.ts index 2a6930a17c..09687f9836 100644 --- a/webapp/packages/plugin-connections/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-connections/src/PluginBootstrap.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -12,8 +12,4 @@ export class PluginBootstrap extends Bootstrap { constructor() { super(); } - - register(): void {} - - load(): void {} } diff --git a/webapp/packages/plugin-connections/src/PluginConnectionsSettingsService.ts b/webapp/packages/plugin-connections/src/PluginConnectionsSettingsService.ts index 22a0cd053a..fa6d2a20a4 100644 --- a/webapp/packages/plugin-connections/src/PluginConnectionsSettingsService.ts +++ b/webapp/packages/plugin-connections/src/PluginConnectionsSettingsService.ts @@ -1,30 +1,50 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { CONNECTIONS_SETTINGS_GROUP } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; -import { SettingsManagerService } from '@cloudbeaver/core-settings'; +import { ESettingsValueType, SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; -import { CONNECTIONS_SETTINGS_GROUP, settings } from './CONNECTIONS_SETTINGS_GROUP'; +const defaultSettings = schema.object({ + 'plugin.connections.hideConnectionViewForUsers': schemaExtra.stringedBoolean().default(false), +}); -const defaultSettings = { - hideConnectionViewForUsers: false, -}; +export type PluginConnectionsSettings = schema.infer; -export type PluginConnectionsSettings = typeof defaultSettings; - -@injectable() +@injectable(() => [SettingsProviderService, SettingsManagerService]) export class PluginConnectionsSettingsService { - readonly settings: PluginSettings; + get hideConnectionViewForUsers(): boolean { + return this.settings.getValue('plugin.connections.hideConnectionViewForUsers'); + } + readonly settings: SettingsProvider; - constructor(private readonly pluginManagerService: PluginManagerService, settingsManagerService: SettingsManagerService) { - this.settings = this.pluginManagerService.createSettings('connections', 'plugin', defaultSettings); + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { + this.settings = this.settingsProviderService.createSettings(defaultSettings); + + this.registerSettings(); + } - settingsManagerService.addGroup(CONNECTIONS_SETTINGS_GROUP); - settingsManagerService.addSettings(settings.scopeType, settings.scope, settings.settingsData); + private registerSettings() { + // todo: probably not working as a separate setting + this.settingsManagerService.registerSettings(() => [ + { + key: 'plugin.connections.hideConnectionViewForUsers', + type: ESettingsValueType.Checkbox, + access: { + scope: ['role'], + }, + name: 'plugin_connections_settings_hide_connections_view_name', + description: 'plugin_connections_settings_hide_connections_view_description', + group: CONNECTIONS_SETTINGS_GROUP, + }, + ]); } } diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.module.css b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.module.css new file mode 100644 index 0000000000..1263d0141b --- /dev/null +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.loader { + height: 100%; +} diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx index 83a1a4f1a1..3bb7e620ac 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx @@ -1,44 +1,32 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useCallback } from 'react'; -import styled, { css } from 'reshadow'; -import { Loader } from '@cloudbeaver/core-blocks'; +import { ColoredContainer, Loader, s, useS } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { ConnectionFormLoader } from '../ConnectionForm/ConnectionFormLoader'; -import { PublicConnectionFormService } from './PublicConnectionFormService'; - -const styles = css` - Loader { - height: 100%; - } -`; +import { ConnectionFormLoader } from '../ConnectionForm/ConnectionFormLoader.js'; +import styles from './PublicConnectionForm.module.css'; +import { PublicConnectionFormService } from './PublicConnectionFormService.js'; export const PublicConnectionForm: React.FC = observer(function PublicConnectionForm() { const service = useService(PublicConnectionFormService); + const style = useS(styles); const close = useCallback(() => service.close(true), []); const save = useCallback(() => service.save(), []); - return styled(styles)( - - {() => - service.formState && ( - - ) - } - , + return ( + + + {service.formState && } + + ); }); diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts index fa12400e1b..a0e3aafa06 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts @@ -1,48 +1,57 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, makeObservable, observable } from 'mobx'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; -import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoResource, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, ConnectionsManagerService, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { injectable, IServiceProvider } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { executorHandlerFilter, ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; -import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; +import { executorHandlerFilter, ExecutorInterrupter, type IExecutorHandler } from '@cloudbeaver/core-executor'; import type { ResourceKey, ResourceKeySimple } from '@cloudbeaver/core-resource'; import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; -import { OptionsPanelService } from '@cloudbeaver/core-ui'; +import { FormMode, OptionsPanelService, type OptionsPanelCloseEventData } from '@cloudbeaver/core-ui'; import { AuthenticationService } from '@cloudbeaver/plugin-authentication'; -import { ConnectionAuthService } from '../ConnectionAuthService'; -import { ConnectionFormService } from '../ConnectionForm/ConnectionFormService'; -import { ConnectionFormState } from '../ConnectionForm/ConnectionFormState'; -import type { IConnectionFormState } from '../ConnectionForm/IConnectionFormProps'; -import { PublicConnectionForm } from './PublicConnectionForm'; +import { ConnectionFormState } from '../ConnectionForm/ConnectionFormState.js'; +import { ConnectionFormService } from '../ConnectionForm/ConnectionFormService.js'; +import { getConnectionFormOptionsPart } from '../ConnectionForm/Options/getConnectionFormOptionsPart.js'; + +const PublicConnectionForm = importLazyComponent(() => import('./PublicConnectionForm.js').then(m => m.PublicConnectionForm)); const formGetter = () => PublicConnectionForm; -@injectable() +@injectable(() => [ + CommonDialogService, + NotificationService, + OptionsPanelService, + IServiceProvider, + ConnectionFormService, + ConnectionInfoResource, + ConnectionsManagerService, + UserInfoResource, + AuthenticationService, +]) export class PublicConnectionFormService { - formState: IConnectionFormState | null; + formState: ConnectionFormState | null; constructor( private readonly commonDialogService: CommonDialogService, private readonly notificationService: NotificationService, private readonly optionsPanelService: OptionsPanelService, + private readonly serviceProvider: IServiceProvider, private readonly connectionFormService: ConnectionFormService, private readonly connectionInfoResource: ConnectionInfoResource, - private readonly connectionAuthService: ConnectionAuthService, + private readonly connectionsManagerService: ConnectionsManagerService, private readonly userInfoResource: UserInfoResource, private readonly authenticationService: AuthenticationService, - private readonly projectsService: ProjectsService, - private readonly projectInfoResource: ProjectInfoResource, ) { this.formState = null; this.optionsPanelService.closeTask.addHandler(this.closeHandler); @@ -53,8 +62,9 @@ export class PublicConnectionFormService { executorHandlerFilter( () => !!this.formState && this.optionsPanelService.isOpen(formGetter), async (event, context) => { - if (event === 'before' && this.userInfoResource.data === null) { + if (event === 'before' && this.userInfoResource.isAnonymous()) { const confirmed = await this.showUnsavedChangesDialog(); + if (!confirmed) { ExecutorInterrupter.interrupt(context); } @@ -71,28 +81,25 @@ export class PublicConnectionFormService { }); } - change(projectId: string, config: ConnectionConfig, availableDrivers?: string[]): void { - // if (this.formState) { - // this.formState.dispose(); - // } - - if (!this.formState) { - this.formState = new ConnectionFormState( - this.projectsService, - this.projectInfoResource, - this.connectionFormService, - this.connectionInfoResource, - ); - - this.formState.closeTask.addHandler(this.close.bind(this, true)); + async change(projectId: string, config: ConnectionConfig, availableDrivers?: string[]): Promise { + this.formState?.dispose(); + this.formState = new ConnectionFormState(this.serviceProvider, this.connectionFormService, { + projectId, + availableDrivers: availableDrivers ?? [], + type: 'public', + requiredNetworkHandlersIds: [], + connectionId: config.connectionId, + }).setMode(config.connectionId ? FormMode.Edit : FormMode.Create); + + await this.optionsPart?.load(); + + if (config.driverId) { + await this.optionsPart?.setDriverId(config.driverId); } - this.formState - .setOptions(config.connectionId ? 'edit' : 'create', 'public') - .setConfig(projectId, config) - .setAvailableDrivers(availableDrivers || []); + Object.assign(this.optionsPart!.state, config); - this.formState.load(); + this.formState.disposeTask.addHandler(this.close.bind(this, true)); } async open(projectId: string, config: ConnectionConfig, availableDrivers?: string[]): Promise { @@ -105,29 +112,20 @@ export class PublicConnectionFormService { return state; } - async close(saved?: boolean): Promise { + async close(saved?: boolean): Promise { if (!this.formState) { - return true; + return; } if (saved) { this.clearFormState(); } - const state = await this.optionsPanelService.close(); - - if (state) { - this.clearFormState(); - } - - return state; + await this.optionsPanelService.close(); } async save(): Promise { - const key = - this.formState && this.formState.config.connectionId && this.formState.projectId !== null - ? createConnectionParam(this.formState.projectId, this.formState.config.connectionId) - : null; + const key = this.optionsPart?.connectionKey; await this.close(true); @@ -136,31 +134,40 @@ export class PublicConnectionFormService { } } + private get optionsPart() { + return this.formState ? getConnectionFormOptionsPart(this.formState) : null; + } + private readonly closeRemoved: IExecutorHandler> = (data, contexts) => { - if (!this.formState || !this.formState.config.connectionId || this.formState.projectId === null) { + if (!this.formState || !this.optionsPart?.connectionKey) { return; } - if (!this.connectionInfoResource.has(createConnectionParam(this.formState.projectId, this.formState.config.connectionId))) { + if (!this.connectionInfoResource.has(this.optionsPart.connectionKey)) { this.close(true); } }; private readonly closeDeleted: IExecutorHandler> = (data, contexts) => { - if (!this.formState || !this.formState.config.connectionId || this.formState.projectId === null) { + if (!this.formState || !this.optionsPart?.connectionKey) { return; } - if (this.connectionInfoResource.isIntersect(data, createConnectionParam(this.formState.projectId, this.formState.config.connectionId))) { + if (this.connectionInfoResource.isIntersect(data, this.optionsPart.connectionKey)) { this.close(true); } }; - private readonly closeHandler: IExecutorHandler = async (data, contexts) => { - const confirmed = await this.showUnsavedChangesDialog(); + private readonly closeHandler: IExecutorHandler = async (data, contexts) => { + if (data === 'before') { + const confirmed = await this.showUnsavedChangesDialog(); + + if (!confirmed) { + ExecutorInterrupter.interrupt(contexts); + return; + } - if (!confirmed) { - ExecutorInterrupter.interrupt(contexts); + this.clearFormState(); } }; @@ -168,22 +175,18 @@ export class PublicConnectionFormService { if ( !this.formState || !this.optionsPanelService.isOpen(formGetter) || - (this.formState.config.connectionId && - this.formState.projectId !== null && - !this.connectionInfoResource.has(createConnectionParam(this.formState.projectId, this.formState.config.connectionId))) + (this.optionsPart?.connectionKey && !this.connectionInfoResource.has(this.optionsPart.connectionKey)) ) { return true; } - const state = await this.formState.checkFormState(); - - if (!state?.edited) { + if (!this.formState.isChanged) { return true; } const result = await this.commonDialogService.open(ConfirmationDialog, { - title: 'connections_public_connection_edit_cancel_title', - message: 'connections_public_connection_edit_cancel_message', + title: 'plugin_connections_connection_edit_cancel_title', + message: 'plugin_connections_connection_edit_cancel_message', confirmActionText: 'ui_processing_ok', }); @@ -192,8 +195,8 @@ export class PublicConnectionFormService { private async tryReconnect(connectionKey: IConnectionInfoParams) { const result = await this.commonDialogService.open(ConfirmationDialog, { - title: 'connections_public_connection_edit_reconnect_title', - message: 'connections_public_connection_edit_reconnect_message', + title: 'plugin_connections_connection_edit_reconnect_title', + message: 'plugin_connections_connection_edit_reconnect_message', confirmActionText: 'ui_reconnect', }); @@ -202,10 +205,10 @@ export class PublicConnectionFormService { } try { - await this.connectionInfoResource.close(connectionKey); - await this.connectionAuthService.auth(connectionKey); + await this.connectionsManagerService.closeConnectionAsync(connectionKey); + await this.connectionsManagerService.requireConnection(connectionKey); } catch (exception: any) { - this.notificationService.logException(exception, 'connections_public_connection_edit_reconnect_failed'); + this.notificationService.logException(exception, 'plugin_connections_connection_edit_reconnect_failed'); } } diff --git a/webapp/packages/plugin-connections/src/index.ts b/webapp/packages/plugin-connections/src/index.ts index d591853d3c..392f963761 100644 --- a/webapp/packages/plugin-connections/src/index.ts +++ b/webapp/packages/plugin-connections/src/index.ts @@ -1,24 +1,38 @@ -import { connectionPlugin } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 * from './ConnectionAuthentication/IConnectionAuthenticationConfig'; -export * from './ConnectionAuthentication/ConnectionAuthenticationFormLoader'; -export * from './ConnectionForm/Options/ConnectionOptionsTabService'; -export * from './ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService'; -export * from './ConnectionForm/SSH/ConnectionSSHTabService'; -export * from './ConnectionForm/OriginInfo/ConnectionOriginInfoTabService'; -export * from './ConnectionForm/Contexts/connectionConfigContext'; -export * from './ConnectionForm/ConnectionFormBaseActionsLoader'; -export * from './ConnectionForm/connectionFormConfigureContext'; -export * from './ConnectionForm/ConnectionFormLoader'; -export * from './ConnectionForm/ConnectionFormService'; -export * from './ConnectionForm/ConnectionFormState'; -export * from './ConnectionForm/IConnectionFormProps'; -export * from './ConnectionForm/useConnectionFormState'; -export * from './ContextMenu/MENU_CONNECTION_VIEW'; -export * from './ContextMenu/MENU_CONNECTIONS'; -export * from './PublicConnectionForm/PublicConnectionFormService'; -export * from './ConnectionAuthService'; -export * from './PluginConnectionsSettingsService'; -export * from './CONNECTIONS_SETTINGS_GROUP'; +import './module.js'; +import { connectionPlugin } from './manifest.js'; + +export * from './ConnectionAuthentication/IConnectionAuthenticationConfig.js'; +export * from './ConnectionAuthentication/ConnectionAuthenticationFormLoader.js'; +export * from './ConnectionForm/Options/ConnectionOptionsTabService.js'; +export * from './ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.js'; +export * from './ConnectionForm/SSH/ConnectionSSHTabService.js'; +export * from './ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.js'; +export * from './ConnectionForm/ConnectionFormBaseActionsLoader.js'; +export * from './ConnectionForm/ConnectionFormLoader.js'; +export * from './ConnectionForm/IConnectionFormState.js'; +export * from './ConnectionForm/ConnectionFormState.js'; +export * from './ConnectionForm/useConnectionFormState.js'; +export * from './ConnectionForm/ConnectionFormService.js'; +export * from './ConnectionForm/Options/getConnectionFormOptionsPart.js'; +export * from './ConnectionForm/Options/ConnectionFormOptionsPart.js'; +export * from './ConnectionForm/SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID.js'; +export * from './ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm.js'; +export * from './ContextMenu/MENU_CONNECTION_VIEW.js'; +export * from './ContextMenu/MENU_CONNECTIONS.js'; +export * from './PublicConnectionForm/PublicConnectionFormService.js'; +export * from './NavNodes/ConnectionNavNodeService.js'; +export * from './ConnectionAuthService.js'; +export * from './PluginConnectionsSettingsService.js'; +export * from './ConnectionShieldLazy.js'; +export * from './Actions/ACTION_TREE_CREATE_CONNECTION.js'; +export * from './Actions/MENU_TREE_CREATE_CONNECTION.js'; export default connectionPlugin; diff --git a/webapp/packages/plugin-connections/src/locales/en.ts b/webapp/packages/plugin-connections/src/locales/en.ts index 98b3b99594..4be794c8b1 100644 --- a/webapp/packages/plugin-connections/src/locales/en.ts +++ b/webapp/packages/plugin-connections/src/locales/en.ts @@ -1,23 +1,50 @@ export default [ - ['connections_public_connection_edit_menu_item_title', 'Edit Connection'], - ['connections_public_connection_edit_cancel_title', 'Cancel confirmation'], - ['connections_public_connection_edit_cancel_message', "You're going to cancel connection changes. Unsaved changes will be lost. Are you sure?"], - ['connections_public_connection_edit_reconnect_title', 'Connection updated'], - ['connections_public_connection_edit_reconnect_message', 'Connection has been updated. Do you want to reconnect?'], - ['connections_public_connection_edit_reconnect_failed', 'Failed to reconnect'], - ['connections_public_connection_folder_move_failed', 'Failed to move to folder'], - ['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({arg:name})'], - ['connections_public_connection_cloud_auth_required', 'You need to sign in with "{arg:providerLabel}" credentials to work with connection.'], + ['plugin_connections_new_connection_dialog_title', 'New connection'], + ['plugin_connections_connection_form_part_main', 'Main'], + ['plugin_connections_connection_form_part_properties', 'Driver Properties'], + ['plugin_connections_connection_form_part_main_custom_host', 'Host'], + ['plugin_connections_connection_form_part_main_custom_port', 'Port'], + ['plugin_connections_connection_form_part_main_custom_server_name', 'Server name'], + ['plugin_connections_connection_form_part_main_custom_database', 'Database'], + ['plugin_connections_connection_form_part_main_url_jdbc', 'JDBC URL'], + ['plugin_connections_connection_form_part_main_folder', 'Folder'], + + ['plugin_connections_connection_edit_menu_item_title', 'Edit Connection'], + ['plugin_connections_connection_edit_cancel_title', 'Cancel confirmation'], + ['plugin_connections_connection_edit_cancel_message', "You're going to cancel connection changes. Unsaved changes will be lost. Are you sure?"], + ['plugin_connections_connection_edit_reconnect_title', 'Connection updated'], + ['plugin_connections_connection_edit_reconnect_message', 'Connection has been updated. Do you want to reconnect?'], + ['plugin_connections_connection_edit_reconnect_failed', 'Failed to reconnect'], + ['plugin_connections_connection_folder_move_failed', 'Failed to move to folder'], + ['plugin_connections_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({arg:name})'], + ['plugin_connections_connection_cloud_auth_required', 'You need to sign in with "{arg:providerLabel}" credentials to work with this connection.'], ['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'], ['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'], ['plugin_connections_connection_form_name_invalid', "Field 'Connection name' can't be empty"], ['plugin_connections_connection_form_host_invalid', "Field 'Host' can't be empty"], - ['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{arg:name}". Connections won\'t be deleted. Are you sure?'], + ['plugin_connections_connection_folder_delete_confirmation', 'You\'re going to delete "{arg:name}". Connections won\'t be deleted. Are you sure?'], ['plugin_connections_menu_connections_label', 'Connection'], ['plugin_connections_action_disconnect_all_label', 'Disconnect All'], - ['settings_connections', 'Connections'], - ['settings_connections_hide_connections_view_name', 'Show connections to admins only'], - ['settings_connections_hide_connections_view_description', 'Show connections to admins only'], + ['plugin_connections_settings', 'Connections'], + ['plugin_connections_settings_hide_connections_view_name', 'Hide connection view management'], + ['plugin_connections_settings_hide_connections_view_description', 'Connections view submenu will be hidden for all users except administrators'], + + ['plugin_connections_connection_ssl_enable', 'Enable SSL'], + ['plugin_connections_connection_ssl_optional', 'All SSL parameters are optional.'], + ['plugin_connections_connection_ssl_description', 'You must specify SSL certificates if they are required by your server configuration. Settings on this page override Driver properties.'], + ['plugin_connections_connection_ssl_note', '{arg:productName} does not verify SSL configuration and relies on the driver implementation. Please refer to the driver documentation for more information.'], + ['plugin_connections_connection_ssl_docs', 'SSL configuration documentation'], - ['connections_public_connection_ssl_enable', 'Enable SSL'], + ['plugin_connections_connection_form_shared_credentials_manage_info', 'You can manage credentials in the '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Credentials tab'], + [ + 'plugin_connections_connection_auth_secret_description', + 'There are multiple credentials available for authentication.\nPlease choose credentials you want to use.', + ], + ['plugin_connections_connection_create_menu_title', 'New Connection'], + ['plugin_connections_connection_driver_not_installed_message', 'Driver is not installed. You can install it in the "Administration" part.'], + ['plugin_connections_connection_established', 'Connection is established'], + ['plugin_connections_connection_client_version', 'Client version: {arg:version} \n'], + ['plugin_connections_connection_server_version', 'Server version: {arg:version} \n'], + ['plugin_connections_connection_connection_time', 'Connection time: {arg:time} \n'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/fr.ts b/webapp/packages/plugin-connections/src/locales/fr.ts new file mode 100644 index 0000000000..dbcaa8a232 --- /dev/null +++ b/webapp/packages/plugin-connections/src/locales/fr.ts @@ -0,0 +1,59 @@ +export default [ + ['plugin_connections_connection_form_part_main', 'Principal'], + ['plugin_connections_connection_form_part_properties', 'Propriétés du pilote'], + ['plugin_connections_connection_form_part_main_custom_host', 'Hôte'], + ['plugin_connections_connection_form_part_main_custom_port', 'Port'], + ['plugin_connections_connection_form_part_main_custom_server_name', 'Nom du serveur'], + ['plugin_connections_connection_form_part_main_custom_database', 'Base de données'], + ['plugin_connections_connection_form_part_main_url_jdbc', 'URL JDBC'], + ['plugin_connections_connection_form_part_main_folder', 'Dossier'], + ['plugin_connections_new_connection_dialog_title', 'Nouvelle connexion'], + + ['plugin_connections_connection_edit_menu_item_title', 'Modifier la connexion'], + ['plugin_connections_connection_edit_cancel_title', "Confirmation d'annulation"], + [ + 'plugin_connections_connection_edit_cancel_message', + 'Vous allez annuler les modifications de la connexion. Les modifications non enregistrées seront perdues. Êtes-vous sûr ?', + ], + ['plugin_connections_connection_edit_reconnect_title', 'Connexion mise à jour'], + ['plugin_connections_connection_edit_reconnect_message', 'La connexion a été mise à jour. Voulez-vous vous reconnecter ?'], + ['plugin_connections_connection_edit_reconnect_failed', 'Échec de la reconnexion'], + ['plugin_connections_connection_folder_move_failed', 'Échec du déplacement vers le dossier'], + [ + 'plugin_connections_connection_folder_move_duplication', + 'Le dossier cible ou les dossiers sélectionnés contiennent un dossier portant le même nom ({arg:name})', + ], + [ + 'plugin_connections_connection_cloud_auth_required', + 'Vous devez vous connecter avec les identifiants "{arg:providerLabel}" pour travailler avec cette connexion.', + ], + ['plugin_connections_connection_form_project_invalid', "Vous n'avez pas accès à la création de connexions dans le projet sélectionné"], + ['plugin_connections_connection_form_host_configuration_invalid', "La configuration de l'hôte n'est pas supportée"], + ['plugin_connections_connection_form_name_invalid', 'Le champ "Nom de la connexion" ne peut pas être vide'], + ['plugin_connections_connection_form_host_invalid', 'Le champ "Hôte" ne peut pas être vide'], + [ + 'plugin_connections_connection_folder_delete_confirmation', + 'Vous allez supprimer "{arg:name}". Les connexions ne seront pas supprimées. Êtes-vous sûr ?', + ], + ['plugin_connections_menu_connections_label', 'Connexion'], + ['plugin_connections_action_disconnect_all_label', 'Déconnecter tout'], + ['plugin_connections_settings', 'Connexions'], + ['plugin_connections_settings_hide_connections_view_name', 'Hide connection view management'], + ['plugin_connections_settings_hide_connections_view_description', 'Connections view submenu will be hidden for all users except administrators'], + + ['plugin_connections_connection_ssl_enable', 'Activer SSL'], + ['plugin_connections_connection_ssl_optional', 'All SSL parameters are optional.'], + ['plugin_connections_connection_ssl_description', 'You must specify SSL certificates if they are required by your server configuration. Settings on this page override Driver properties'], + ['plugin_connections_connection_ssl_note', '{arg:productName} does not verify SSL configuration and relies on the driver implementation. Please refer to the driver documentation for more information.'], + ['plugin_connections_connection_ssl_docs', 'SSL configuration documentation'], + + ['plugin_connections_connection_form_shared_credentials_manage_info', "Vous pouvez gérer les identifiants dans l'onglet "], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Onglet Identifiants'], + ['plugin_connections_connection_auth_secret_description', 'Veuillez sélectionner les identifiants fournis par une de vos équipes'], + ['plugin_connections_connection_create_menu_title', 'New Connection'], + ['plugin_connections_connection_driver_not_installed_message', 'Driver is not installed. You can install it in the "Administration" part.'], + ['plugin_connections_connection_established', 'Connection is established'], + ['plugin_connections_connection_client_version', 'Client version: {arg:version} \n'], + ['plugin_connections_connection_server_version', 'Server version: {arg:version} \n'], + ['plugin_connections_connection_connection_time', 'Connection time: {arg:time} \n'], +]; diff --git a/webapp/packages/plugin-connections/src/locales/it.ts b/webapp/packages/plugin-connections/src/locales/it.ts index c13c7bda6c..5dbd2833db 100644 --- a/webapp/packages/plugin-connections/src/locales/it.ts +++ b/webapp/packages/plugin-connections/src/locales/it.ts @@ -1,25 +1,52 @@ export default [ - ['connections_public_connection_edit_menu_item_title', 'Modifica Connessione'], - ['connections_public_connection_edit_cancel_title', "Conferma l'annullamento"], + ['plugin_connections_connection_form_part_main', 'Main'], + ['plugin_connections_connection_form_part_properties', 'Proprietà del driver'], + ['plugin_connections_connection_form_part_main_custom_host', 'Host'], + ['plugin_connections_connection_form_part_main_custom_port', 'Porta'], + ['plugin_connections_connection_form_part_main_custom_server_name', 'Server name'], + ['plugin_connections_connection_form_part_main_custom_database', 'Database'], + ['plugin_connections_connection_form_part_main_url_jdbc', 'JDBC URL'], + ['plugin_connections_connection_form_part_main_folder', 'Folder'], + ['plugin_connections_new_connection_dialog_title', 'Nuova connessione'], + + ['plugin_connections_connection_edit_menu_item_title', 'Modifica Connessione'], + ['plugin_connections_connection_edit_cancel_title', "Conferma l'annullamento"], [ - 'connections_public_connection_edit_cancel_message', + 'plugin_connections_connection_edit_cancel_message', 'Stai per annullare le modifiche alla connessione. Modifiche non salvate saranno perse. Sei sicuro?', ], - ['connections_public_connection_edit_reconnect_title', 'Connection updated'], - ['connections_public_connection_edit_reconnect_message', 'Connection has been updated. Do you want to reconnect?'], - ['connections_public_connection_edit_reconnect_failed', 'Failed to reconnect'], - ['connections_public_connection_folder_move_failed', 'Failed to move to folder'], - ['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({arg:name})'], + ['plugin_connections_connection_edit_reconnect_title', 'Connection updated'], + ['plugin_connections_connection_edit_reconnect_message', 'Connection has been updated. Do you want to reconnect?'], + ['plugin_connections_connection_edit_reconnect_failed', 'Failed to reconnect'], + ['plugin_connections_connection_folder_move_failed', 'Failed to move to folder'], + ['plugin_connections_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({arg:name})'], ['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'], ['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'], ['plugin_connections_connection_form_name_invalid', "Field 'Connection name' can't be empty"], ['plugin_connections_connection_form_host_invalid', "Field 'Host' can't be empty"], - ['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{arg:name}". Connections won\'t be deleted. Are you sure?'], + ['plugin_connections_connection_folder_delete_confirmation', 'You\'re going to delete "{arg:name}". Connections won\'t be deleted. Are you sure?'], ['plugin_connections_menu_connections_label', 'Connessione'], ['plugin_connections_action_disconnect_all_label', 'Scollegati da tutto'], - ['settings_connections', 'Connections'], - ['settings_connections_hide_connections_view_name', 'Show connections to admins only'], - ['settings_connections_hide_connections_view_description', 'Show connections to admins only'], + ['plugin_connections_settings', 'Connections'], + ['plugin_connections_settings_hide_connections_view_name', 'Hide connection view management'], + ['plugin_connections_settings_hide_connections_view_description', 'Connections view submenu will be hidden for all users except administrators'], + + ['plugin_connections_connection_ssl_enable', 'Enable SSL'], + ['plugin_connections_connection_ssl_optional', 'All SSL parameters are optional.'], + ['plugin_connections_connection_ssl_description', 'You must specify SSL certificates if they are required by your server configuration. Settings on this page override Driver properties'], + ['plugin_connections_connection_ssl_note', '{arg:productName} does not verify SSL configuration and relies on the driver implementation. Please refer to the driver documentation for more information.'], + ['plugin_connections_connection_ssl_docs', 'SSL configuration documentation'], - ['connections_public_connection_ssl_enable', 'Enable SSL'], + ['plugin_connections_connection_form_shared_credentials_manage_info', 'You can manage credentials in the '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Credentials tab'], + [ + 'plugin_connections_connection_auth_secret_description', + 'There are multiple credentials available for authentication.\nPlease choose credentials you want to use.', + ], + ['plugin_connections_connection_create_menu_title', 'New Connection'], + ['plugin_connections_connection_driver_not_installed_message', 'Driver is not installed. You can install it in the "Administration" part.'], + ['plugin_connections_connection_established', 'Connection is established'], + ['plugin_connections_connection_client_version', 'Client version: {arg:version} \n'], + ['plugin_connections_connection_server_version', 'Server version: {arg:version} \n'], + ['plugin_connections_connection_connection_time', 'Connection time: {arg:time} \n'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/ru.ts b/webapp/packages/plugin-connections/src/locales/ru.ts index 711186564a..19f2919d9d 100644 --- a/webapp/packages/plugin-connections/src/locales/ru.ts +++ b/webapp/packages/plugin-connections/src/locales/ru.ts @@ -1,22 +1,50 @@ export default [ - ['connections_public_connection_edit_menu_item_title', 'Изменить подключение'], - ['connections_public_connection_edit_cancel_title', 'Отмена редактирования'], - ['connections_public_connection_edit_cancel_message', 'Вы собираетесь закрыть редактор, несохраненные изменения не будут применены. Вы уверены?'], - ['connections_public_connection_edit_reconnect_title', 'Подключение обновлено'], - ['connections_public_connection_edit_reconnect_message', 'Подключение было обновлено. Вы хотите переподключиться?'], - ['connections_public_connection_edit_reconnect_failed', 'Не удалось переподключиться'], - ['connections_public_connection_folder_move_failed', 'Ошибка перемещения в папку'], - ['connections_public_connection_folder_move_duplication', 'Выбранные папки или папка назначения содержит папки с таким же названием ({arg:name})'], + ['plugin_connections_connection_form_part_main', 'Главное'], + ['plugin_connections_connection_form_part_properties', 'Параметры драйвера'], + ['plugin_connections_connection_form_part_main_custom_host', 'Хост'], + ['plugin_connections_connection_form_part_main_custom_port', 'Порт'], + ['plugin_connections_connection_form_part_main_custom_server_name', 'Имя сервера'], + ['plugin_connections_connection_form_part_main_custom_database', 'База'], + ['plugin_connections_connection_form_part_main_url_jdbc', 'JDBC URL'], + ['plugin_connections_connection_form_part_main_folder', 'Папка'], + ['plugin_connections_new_connection_dialog_title', 'Новое подключение'], + + ['plugin_connections_connection_edit_menu_item_title', 'Изменить подключение'], + ['plugin_connections_connection_edit_cancel_title', 'Отмена редактирования'], + ['plugin_connections_connection_edit_cancel_message', 'Вы собираетесь закрыть редактор, несохраненные изменения не будут применены. Вы уверены?'], + ['plugin_connections_connection_edit_reconnect_title', 'Подключение обновлено'], + ['plugin_connections_connection_edit_reconnect_message', 'Подключение было обновлено. Вы хотите переподключиться?'], + ['plugin_connections_connection_edit_reconnect_failed', 'Не удалось переподключиться'], + ['plugin_connections_connection_folder_move_failed', 'Ошибка перемещения в папку'], + ['plugin_connections_connection_folder_move_duplication', 'Выбранные папки или папка назначения содержит папки с таким же названием ({arg:name})'], + ['plugin_connections_connection_cloud_auth_required', 'Вы должны подключиться через "{arg:providerLabel}", чтобы работать с этим соединением.'], ['plugin_connections_connection_form_project_invalid', 'У вас нет разрешения создавать коннекшены в выбранном проекте'], ['plugin_connections_connection_form_host_configuration_invalid', 'Конфигурация хоста не поддерживается'], ['plugin_connections_connection_form_name_invalid', "Поле 'Название подключения' не может быть пустым"], ['plugin_connections_connection_form_host_invalid', "Поле 'Хост' не может быть пустым"], - ['connections_public_connection_folder_delete_confirmation', 'Вы удаляете "{arg:name}". Подключения не будут удалены. Вы уверены?'], + ['plugin_connections_connection_folder_delete_confirmation', 'Вы удаляете "{arg:name}". Подключения не будут удалены. Вы уверены?'], ['plugin_connections_menu_connections_label', 'Подключение'], ['plugin_connections_action_disconnect_all_label', 'Отключить все'], - ['settings_connections', 'Подключения'], - ['settings_connections_hide_connections_view_name', 'Показывать подключения только администраторам'], - ['settings_connections_hide_connections_view_description', 'Показывать подключения только администраторам'], + ['plugin_connections_settings', 'Подключения'], + ['plugin_connections_settings_hide_connections_view_name', 'Скрыть управление отображением подключений'], + ['plugin_connections_settings_hide_connections_view_description', 'Подменю выбора отображения подключения будет скрыто для всех пользователей, кроме администраторов'], + + ['plugin_connections_connection_ssl_enable', 'Включить SSL'], + ['plugin_connections_connection_ssl_optional', 'Все параметры SSL являются необязательными.'], + ['plugin_connections_connection_ssl_description', 'Вы должны указать SSL сертификаты, если они требуются для конфигурации вашего сервера. Настройки на этой странице переопределяют свойства драйвера'], + ['plugin_connections_connection_ssl_note', '{arg:productName} не проверяет конфигурацию SSL и полагается на реализацию драйвера. Пожалуйста, обратитесь к документации драйвера для получения дополнительной информации.'], + ['plugin_connections_connection_ssl_docs', 'Документация по настройке SSL'], - ['connections_public_connection_ssl_enable', 'Включить SSL'], + ['plugin_connections_connection_form_shared_credentials_manage_info', 'Вы можете указать учетные данные в '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'во вкладке "Учетные данные"'], + [ + 'plugin_connections_connection_auth_secret_description', + 'У вас есть несколько учетных записей для авторизации.\nВыберите учетную запись из списка.', + ], + ['plugin_connections_connection_create_menu_title', 'Новое Подключение'], + ['plugin_connections_connection_driver_not_installed_message', 'Драйвер не установлен. Вы можете установить его в "Администрированой" части.'], + ['plugin_connections_connection_established', 'Подключение установлено'], + ['plugin_connections_connection_client_version', 'Версия клиента: {arg:version} \n'], + ['plugin_connections_connection_server_version', 'Версия сервера: {arg:version} \n'], + ['plugin_connections_connection_connection_time', 'Время подключения: {arg:time} \n'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/vi.ts b/webapp/packages/plugin-connections/src/locales/vi.ts new file mode 100644 index 0000000000..0be2fc989a --- /dev/null +++ b/webapp/packages/plugin-connections/src/locales/vi.ts @@ -0,0 +1,53 @@ +export default [ + ['plugin_connections_new_connection_dialog_title', 'Kết nối mới'], + ['plugin_connections_connection_form_part_main', 'Chính'], + ['plugin_connections_connection_form_part_properties', 'Thuộc tính Trình điều khiển (driver)'], + ['plugin_connections_connection_form_part_main_custom_host', 'Máy chủ'], + ['plugin_connections_connection_form_part_main_custom_port', 'Cổng'], + ['plugin_connections_connection_form_part_main_custom_server_name', 'Tên máy chủ'], + ['plugin_connections_connection_form_part_main_custom_database', 'Cơ sở dữ liệu'], + ['plugin_connections_connection_form_part_main_url_jdbc', 'URL JDBC'], + ['plugin_connections_connection_form_part_main_folder', 'Thư mục'], + + ['plugin_connections_connection_edit_menu_item_title', 'Chỉnh sửa Kết nối'], + ['plugin_connections_connection_edit_cancel_title', 'Xác nhận Hủy'], + ['plugin_connections_connection_edit_cancel_message', 'Bạn sắp hủy các thay đổi của kết nối. Các thay đổi chưa lưu sẽ bị mất. Bạn có chắc không?'], + ['plugin_connections_connection_edit_reconnect_title', 'Kết nối đã được cập nhật'], + ['plugin_connections_connection_edit_reconnect_message', 'Kết nối đã được cập nhật. Bạn có muốn kết nối lại không?'], + ['plugin_connections_connection_edit_reconnect_failed', 'Không thể kết nối lại'], + ['plugin_connections_connection_folder_move_failed', 'Không thể di chuyển đến thư mục'], + ['plugin_connections_connection_folder_move_duplication', 'Thư mục đích hoặc các thư mục được chọn chứa thư mục có cùng tên ({arg:name})'], + ['plugin_connections_connection_cloud_auth_required', 'Bạn cần đăng nhập bằng thông tin xác thực "{arg:providerLabel}" để sử dụng kết nối này.'], + ['plugin_connections_connection_form_project_invalid', 'Bạn không có quyền tạo kết nối trong dự án đã chọn'], + ['plugin_connections_connection_form_host_configuration_invalid', 'Cấu hình máy chủ không được hỗ trợ'], + ['plugin_connections_connection_form_name_invalid', "Trường 'Tên kết nối' không được để trống"], + ['plugin_connections_connection_form_host_invalid', "Trường 'Máy chủ' không được để trống"], + ['plugin_connections_connection_folder_delete_confirmation', 'Bạn sắp xóa "{arg:name}". Các kết nối sẽ không bị xóa. Bạn có chắc không?'], + ['plugin_connections_menu_connections_label', 'Kết nối'], + ['plugin_connections_action_disconnect_all_label', 'Ngắt tất cả kết nối'], + ['plugin_connections_settings', 'Kết nối'], + ['plugin_connections_settings_hide_connections_view_name', 'Ẩn kết nối'], + ['plugin_connections_settings_hide_connections_view_description', 'Kết nối sẽ được ẩn đối với tất cả người dùng, trừ quản trị viên'], + + ['plugin_connections_connection_ssl_enable', 'Bật SSL'], + ['plugin_connections_connection_ssl_optional', 'All SSL parameters are optional.'], + ['plugin_connections_connection_ssl_description', 'You must specify SSL certificates if they are required by your server configuration. Settings on this page override Driver properties'], + ['plugin_connections_connection_ssl_note', '{arg:productName} does not verify SSL configuration and relies on the driver implementation. Please refer to the driver documentation for more information.'], + ['plugin_connections_connection_ssl_docs', 'SSL configuration documentation'], + + ['plugin_connections_connection_form_shared_credentials_manage_info', 'Bạn có thể quản lý thông tin xác thực trong '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Tab Thông tin xác thực'], + [ + 'plugin_connections_connection_auth_secret_description', + 'Có nhiều thông tin xác thực có sẵn để xác thực.\nVui lòng chọn thông tin xác thực bạn muốn sử dụng.', + ], + ['plugin_connections_connection_create_menu_title', 'Kết nối mới'], + [ + 'plugin_connections_connection_driver_not_installed_message', + 'Trình điều khiển (driver) chưa được cài đặt. Bạn có thể cài đặt nó trong phần "Quản trị".', + ], + ['plugin_connections_connection_established', 'Kết nối đã được thiết lập'], + ['plugin_connections_connection_client_version', 'Phiên bản máy khách: {arg:version} \n'], + ['plugin_connections_connection_server_version', 'Phiên bản máy chủ: {arg:version} \n'], + ['plugin_connections_connection_connection_time', 'Thời gian kết nối: {arg:time} \n'], +]; diff --git a/webapp/packages/plugin-connections/src/locales/zh.ts b/webapp/packages/plugin-connections/src/locales/zh.ts index e8c876924c..0cbc6e3391 100644 --- a/webapp/packages/plugin-connections/src/locales/zh.ts +++ b/webapp/packages/plugin-connections/src/locales/zh.ts @@ -1,22 +1,46 @@ export default [ - ['connections_public_connection_edit_menu_item_title', '编辑连接'], - ['connections_public_connection_edit_cancel_title', '取消确认'], - ['connections_public_connection_edit_cancel_message', '您将取消连接更改。未保存的更改将丢失。您确定吗?'], - ['connections_public_connection_edit_reconnect_title', '连接已更新'], - ['connections_public_connection_edit_reconnect_message', '连接已更新。您想重新连接吗?'], - ['connections_public_connection_edit_reconnect_failed', '重新连接失败'], - ['connections_public_connection_folder_move_failed', 'Failed to move to folder'], - ['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({arg:name})'], - ['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'], - ['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'], - ['plugin_connections_connection_form_name_invalid', "Field 'Connection name' can't be empty"], - ['plugin_connections_connection_form_host_invalid', "Field 'Host' can't be empty"], - ['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{arg:name}". Connections won\'t be deleted. Are you sure?'], + ['plugin_connections_connection_form_part_main', '主要'], + ['plugin_connections_connection_form_part_properties', '驱动属性'], + ['plugin_connections_connection_form_part_main_custom_host', '主机'], + ['plugin_connections_connection_form_part_main_custom_port', '端口'], + ['plugin_connections_connection_form_part_main_custom_server_name', '服务器名称'], + ['plugin_connections_connection_form_part_main_custom_database', '数据库'], + ['plugin_connections_connection_form_part_main_url_jdbc', 'JDBC URL'], + ['plugin_connections_connection_form_part_main_folder', '文件夹'], + ['plugin_connections_new_connection_dialog_title', '新连接'], + + ['plugin_connections_connection_edit_menu_item_title', '编辑连接'], + ['plugin_connections_connection_edit_cancel_title', '取消确认'], + ['plugin_connections_connection_edit_cancel_message', '您将取消连接更改。未保存的更改将丢失。您确定吗?'], + ['plugin_connections_connection_edit_reconnect_title', '连接已更新'], + ['plugin_connections_connection_edit_reconnect_message', '连接已更新。您想重新连接吗?'], + ['plugin_connections_connection_edit_reconnect_failed', '重新连接失败'], + ['plugin_connections_connection_folder_move_failed', '移动到文件夹失败'], + ['plugin_connections_connection_folder_move_duplication', '目标文件夹或所选文件夹包含同名文件夹 ({arg:name})'], + ['plugin_connections_connection_form_project_invalid', '您无权在所选项目中创建连接'], + ['plugin_connections_connection_form_host_configuration_invalid', '不支持主机配置'], + ['plugin_connections_connection_form_name_invalid', "字段 '连接名称' 不可为空"], + ['plugin_connections_connection_form_host_invalid', "字段 '主机' 不可为空"], + ['plugin_connections_connection_folder_delete_confirmation', '即将删除 "{arg:name}". 连接并不会删除,是否确定?'], ['plugin_connections_menu_connections_label', '连接'], ['plugin_connections_action_disconnect_all_label', '断开所有连接'], - ['settings_connections', 'Connections'], - ['settings_connections_hide_connections_view_name', 'Show connections to admins only'], - ['settings_connections_hide_connections_view_description', 'Show connections to admins only'], + ['plugin_connections_settings', '连接'], + ['plugin_connections_settings_hide_connections_view_name', 'Hide connection view management'], + ['plugin_connections_settings_hide_connections_view_description', 'Connections view submenu will be hidden for all users except administrators'], + + ['plugin_connections_connection_ssl_enable', '启用 SSL'], + ['plugin_connections_connection_ssl_optional', 'All SSL parameters are optional.'], + ['plugin_connections_connection_ssl_description', 'You must specify SSL certificates if they are required by your server configuration. Settings on this page override Driver properties'], + ['plugin_connections_connection_ssl_note', '{arg:productName} does not verify SSL configuration and relies on the driver implementation. Please refer to the driver documentation for more information.'], + ['plugin_connections_connection_ssl_docs', 'SSL configuration documentation'], - ['connections_public_connection_ssl_enable', 'Enable SSL'], + ['plugin_connections_connection_form_shared_credentials_manage_info', '您可在此管理凭证 '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', '凭证页签'], + ['plugin_connections_connection_auth_secret_description', '有多个凭证可用于身份验证.\n请选择您要使用的凭证。'], + ['plugin_connections_connection_create_menu_title', 'New Connection'], + ['plugin_connections_connection_driver_not_installed_message', 'Driver is not installed. You can install it in the "Administration" part.'], + ['plugin_connections_connection_established', 'Connection is established'], + ['plugin_connections_connection_client_version', 'Client version: {arg:version} \n'], + ['plugin_connections_connection_server_version', 'Server version: {arg:version} \n'], + ['plugin_connections_connection_connection_time', 'Connection time: {arg:time} \n'], ]; diff --git a/webapp/packages/plugin-connections/src/manifest.ts b/webapp/packages/plugin-connections/src/manifest.ts index a1dfe7c01a..f64c3665c3 100644 --- a/webapp/packages/plugin-connections/src/manifest.ts +++ b/webapp/packages/plugin-connections/src/manifest.ts @@ -1,44 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { ConnectionAuthService } from './ConnectionAuthService'; -import { ConnectionFormService } from './ConnectionForm/ConnectionFormService'; -import { ConnectionDriverPropertiesTabService } from './ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService'; -import { ConnectionOptionsTabService } from './ConnectionForm/Options/ConnectionOptionsTabService'; -import { ConnectionOriginInfoTabService } from './ConnectionForm/OriginInfo/ConnectionOriginInfoTabService'; -import { ConnectionSSHTabService } from './ConnectionForm/SSH/ConnectionSSHTabService'; -import { ConnectionMenuBootstrap } from './ContextMenu/ConnectionMenuBootstrap'; -import { LocaleService } from './LocaleService'; -import { ConnectionFoldersBootstrap } from './NavNodes/ConnectionFoldersBootstrap'; -import { PluginBootstrap } from './PluginBootstrap'; -import { PluginConnectionsSettingsService } from './PluginConnectionsSettingsService'; -import { PublicConnectionFormService } from './PublicConnectionForm/PublicConnectionFormService'; -import { ConnectionSSLTabService } from './ConnectionForm/SSL/ConnectionSSLTabService'; - export const connectionPlugin: PluginManifest = { info: { name: 'Connections plugin', }, - - providers: [ - PluginBootstrap, - ConnectionMenuBootstrap, - PublicConnectionFormService, - LocaleService, - ConnectionAuthService, - ConnectionFormService, - ConnectionOptionsTabService, - ConnectionDriverPropertiesTabService, - ConnectionSSHTabService, - ConnectionOriginInfoTabService, - ConnectionFoldersBootstrap, - ConnectionSSLTabService, - PluginConnectionsSettingsService, - ], }; diff --git a/webapp/packages/plugin-connections/src/module.ts b/webapp/packages/plugin-connections/src/module.ts new file mode 100644 index 0000000000..40bd9a7628 --- /dev/null +++ b/webapp/packages/plugin-connections/src/module.ts @@ -0,0 +1,58 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { PluginConnectionsSettingsService } from './PluginConnectionsSettingsService.js'; +import { PublicConnectionFormService } from './PublicConnectionForm/PublicConnectionFormService.js'; +import { ConnectionsExplorerBootstrap } from './NavNodes/ConnectionsExplorerBootstrap.js'; +import { PluginBootstrap } from './PluginBootstrap.js'; +import { ConnectionNavNodeService } from './NavNodes/ConnectionNavNodeService.js'; +import { ConnectionFoldersBootstrap } from './NavNodes/ConnectionFoldersBootstrap.js'; +import { LocaleService } from './LocaleService.js'; +import { ConnectionMenuBootstrap } from './ContextMenu/ConnectionMenuBootstrap.js'; +import { ConnectionSSLTabService } from './ConnectionForm/SSL/ConnectionSSLTabService.js'; +import { ConnectionOriginInfoTabService } from './ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.js'; +import { ConnectionSSHTabService } from './ConnectionForm/SSH/ConnectionSSHTabService.js'; +import { ConnectionOptionsTabService } from './ConnectionForm/Options/ConnectionOptionsTabService.js'; +import { ConnectionDriverPropertiesTabService } from './ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.js'; +import { ConnectionFormService } from './ConnectionForm/ConnectionFormService.js'; +import { ConnectionAuthService } from './ConnectionAuthService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-connections', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, proxy(ConnectionDriverPropertiesTabService)) + .addSingleton(Bootstrap, proxy(ConnectionFoldersBootstrap)) + .addSingleton(Bootstrap, proxy(ConnectionMenuBootstrap)) + .addSingleton(Bootstrap, proxy(ConnectionOptionsTabService)) + .addSingleton(Bootstrap, proxy(ConnectionOriginInfoTabService)) + .addSingleton(Bootstrap, proxy(ConnectionsExplorerBootstrap)) + .addSingleton(Bootstrap, proxy(ConnectionSSHTabService)) + .addSingleton(Bootstrap, proxy(ConnectionSSLTabService)) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(Bootstrap, PluginBootstrap) + .addSingleton(Dependency, proxy(ConnectionAuthService)) + .addSingleton(Dependency, proxy(PluginConnectionsSettingsService)) + .addSingleton(Dependency, proxy(ConnectionNavNodeService)) + .addSingleton(ConnectionAuthService) + .addSingleton(PluginConnectionsSettingsService) + .addSingleton(PublicConnectionFormService) + .addSingleton(ConnectionsExplorerBootstrap) + .addSingleton(ConnectionNavNodeService) + .addSingleton(ConnectionFoldersBootstrap) + .addSingleton(ConnectionMenuBootstrap) + .addSingleton(ConnectionSSLTabService) + .addSingleton(ConnectionOriginInfoTabService) + .addSingleton(ConnectionSSHTabService) + .addSingleton(ConnectionOptionsTabService) + .addSingleton(ConnectionDriverPropertiesTabService) + .addSingleton(ConnectionFormService); + }, +}); diff --git a/webapp/packages/plugin-connections/tsconfig.json b/webapp/packages/plugin-connections/tsconfig.json index d024b2519b..28285cbd4c 100644 --- a/webapp/packages/plugin-connections/tsconfig.json +++ b/webapp/packages/plugin-connections/tsconfig.json @@ -1,79 +1,89 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-authentication/tsconfig.json" + "path": "../../common-typescript/@dbeaver/jdbc-uri-parser" }, { - "path": "../plugin-navigation-tree/tsconfig.json" + "path": "../../common-typescript/@dbeaver/js-helpers" }, { - "path": "../plugin-projects/tsconfig.json" + "path": "../core-authentication" }, { - "path": "../plugin-top-app-bar/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-authentication/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-connections" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../core-data-context/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-executor" }, { - "path": "../core-executor/tsconfig.json" + "path": "../core-links" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-navigation-tree/tsconfig.json" + "path": "../core-navigation-tree" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-projects" }, { - "path": "../core-projects/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-sdk" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-settings" }, { - "path": "../core-settings/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-utils" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-view" }, { - "path": "../core-view/tsconfig.json" + "path": "../plugin-authentication" + }, + { + "path": "../plugin-navigation-tree" + }, + { + "path": "../plugin-projects" + }, + { + "path": "../plugin-top-app-bar" } ], "include": [ @@ -85,7 +95,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-d3js/package.json b/webapp/packages/plugin-d3js/package.json index 10ad418fae..1f3fc0fa46 100644 --- a/webapp/packages/plugin-d3js/package.json +++ b/webapp/packages/plugin-d3js/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-d3js", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,24 +11,28 @@ "version": "0.1.0", "description": "The plugin reexports d3js library and contains utility functions and components for d3js", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "pretest": "tsc -b", - "test": "core-cli-test", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "test": "dbeaver-test", + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "d3": "^7.8.5", - "d3-drag": "^3.0.0", - "@cloudbeaver/core-di": "~0.1.0" + "@cloudbeaver/core-di": "workspace:*", + "d3": "^7", + "d3-drag": "^3", + "tslib": "^2" }, "devDependencies": { - "@types/d3": "^7.4.0", - "@types/d3-drag": "^3.0.3" - }, - "peerDependencies": {} + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@dbeaver/cli": "workspace:*", + "@types/d3": "^7", + "@types/d3-drag": "^3", + "typescript": "^5" + } } diff --git a/webapp/packages/plugin-d3js/src/index.ts b/webapp/packages/plugin-d3js/src/index.ts index 60b2f65e00..d259822863 100644 --- a/webapp/packages/plugin-d3js/src/index.ts +++ b/webapp/packages/plugin-d3js/src/index.ts @@ -1,4 +1,12 @@ -import { pluginD3js } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { pluginD3js } from './manifest.js'; export default pluginD3js; @@ -13,9 +21,28 @@ export { format, scaleBand, max, + min, scaleLinear, interpolateRound, axisBottom, axisLeft, + scaleOrdinal, + pie, + arc, + scalePoint, + schemeTableau10, + sum, +} from 'd3'; +export type { + Selection, + ZoomBehavior, + Line, + DragBehavior, + SubjectPosition, + PieArcDatum, + ScaleOrdinal, + Axis, + NumberValue, + ScaleLinear, + ScaleBand, } from 'd3'; -export type { Selection, ZoomBehavior, Line, DragBehavior, SubjectPosition } from 'd3'; diff --git a/webapp/packages/plugin-d3js/src/manifest.ts b/webapp/packages/plugin-d3js/src/manifest.ts index 825a539d6f..2ebe5ad396 100644 --- a/webapp/packages/plugin-d3js/src/manifest.ts +++ b/webapp/packages/plugin-d3js/src/manifest.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -11,5 +11,4 @@ export const pluginD3js: PluginManifest = { info: { name: 'D3js Plugin', }, - providers: [], }; diff --git a/webapp/packages/plugin-d3js/tsconfig.json b/webapp/packages/plugin-d3js/tsconfig.json index 4a48d52a9c..2f98b7d8db 100644 --- a/webapp/packages/plugin-d3js/tsconfig.json +++ b/webapp/packages/plugin-d3js/tsconfig.json @@ -1,13 +1,20 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../core-di/tsconfig.json" + "path": "../../common-typescript/@dbeaver/cli" + }, + { + "path": "../core-cli" + }, + { + "path": "../core-di" } ], "include": [ @@ -19,7 +26,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-data-editor-public-settings/package.json b/webapp/packages/plugin-data-editor-public-settings/package.json new file mode 100644 index 0000000000..9354d03ac7 --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/package.json @@ -0,0 +1,41 @@ +{ + "name": "@cloudbeaver/plugin-data-editor-public-settings", + "type": "module", + "sideEffects": [ + "./lib/module.js", + "./lib/index.js", + "src/**/*.css", + "src/**/*.scss", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "exports": { + ".": "./lib/index.js" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf --glob lib", + "lint": "eslint ./src/ --ext .ts,.tsx", + "test": "dbeaver-test", + "validate-dependencies": "core-cli-validate-dependencies" + }, + "dependencies": { + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-settings": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/plugin-data-import": "workspace:*", + "@cloudbeaver/plugin-data-viewer": "workspace:*" + }, + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@dbeaver/cli": "workspace:*", + "rimraf": "^6", + "tslib": "^2", + "typescript": "^5" + } +} diff --git a/webapp/packages/plugin-data-editor-public-settings/src/LocaleService.ts b/webapp/packages/plugin-data-editor-public-settings/src/LocaleService.ts new file mode 100644 index 0000000000..58ef49c9f0 --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/LocaleService.ts @@ -0,0 +1,39 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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(() => [LocalizationService]) +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + override register(): void { + this.localizationService.addProvider(this.provider.bind(this)); + } + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru.js')).default; + case 'it': + return (await import('./locales/it.js')).default; + case 'zh': + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'de': + return (await import('./locales/de.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; + default: + return (await import('./locales/en.js')).default; + } + } +} diff --git a/webapp/packages/plugin-data-editor-public-settings/src/PluginDataEditorPublicSettingsBootstrap.ts b/webapp/packages/plugin-data-editor-public-settings/src/PluginDataEditorPublicSettingsBootstrap.ts new file mode 100644 index 0000000000..d87a0658ad --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/PluginDataEditorPublicSettingsBootstrap.ts @@ -0,0 +1,67 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ESettingsValueType, SettingsManagerService } from '@cloudbeaver/core-settings'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { DATA_EDITOR_SETTINGS_GROUP, type DataViewerSettingsSchema } from '@cloudbeaver/plugin-data-viewer'; +import type { DataImportSettingsSchema } from '@cloudbeaver/plugin-data-import'; + +@injectable(() => [SettingsManagerService]) +export class PluginDataEditorPublicSettingsBootstrap extends Bootstrap { + constructor(private readonly settingsManagerService: SettingsManagerService) { + super(); + } + + override register(): void { + this.settingsManagerService.registerSettings(() => [ + { + key: 'plugin.data-viewer.disableEdit', + access: { + scope: ['server', 'role'], + }, + type: ESettingsValueType.Checkbox, + name: 'data_editor_public_settings_disable_edit_name', + description: 'data_editor_public_settings_disable_edit_description', + group: DATA_EDITOR_SETTINGS_GROUP, + }, + { + key: 'plugin.data-viewer.disableCopyData', + access: { + scope: ['server', 'role'], + }, + type: ESettingsValueType.Checkbox, + name: 'data_editor_public_settings_disable_data_copy_name', + description: 'data_editor_public_settings_disable_data_copy_description', + group: DATA_EDITOR_SETTINGS_GROUP, + }, + { + group: DATA_EDITOR_SETTINGS_GROUP, + key: 'plugin.data-viewer.export.disabled', + type: ESettingsValueType.Checkbox, + name: 'data_editor_public_settings_disable_data_export_name', + description: 'data_editor_public_settings_disable_data_export_description', + access: { + scope: ['server', 'role'], + }, + }, + ]); + + this.settingsManagerService.registerSettings(() => [ + { + group: DATA_EDITOR_SETTINGS_GROUP, + key: 'plugin.data-import.disabled', + type: ESettingsValueType.Checkbox, + name: 'data_editor_public_settings_disable_data_import_name', + description: 'data_editor_public_settings_disable_data_import_description', + access: { + scope: ['server', 'role'], + }, + }, + ]); + } +} diff --git a/webapp/packages/plugin-data-editor-public-settings/src/index.ts b/webapp/packages/plugin-data-editor-public-settings/src/index.ts new file mode 100644 index 0000000000..feaa728359 --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/index.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; + +import { pluginDataEditorPublicSettingsManifest } from './manifest.js'; + +export default pluginDataEditorPublicSettingsManifest; +export { pluginDataEditorPublicSettingsManifest }; diff --git a/webapp/packages/plugin-data-editor-public-settings/src/locales/de.ts b/webapp/packages/plugin-data-editor-public-settings/src/locales/de.ts new file mode 100644 index 0000000000..7c2c7d47ee --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/locales/de.ts @@ -0,0 +1,10 @@ +export default [ + ['data_editor_public_settings_disable_edit_name', 'Bearbeiten deaktivieren'], + ['data_editor_public_settings_disable_edit_description', 'Deaktivieren Sie die Bearbeitung von Daten in Data Editor für Nicht-Admin-Benutzer'], + ['data_editor_public_settings_disable_data_copy_name', 'Kopie deaktivieren'], + ['data_editor_public_settings_disable_data_copy_description', 'Deaktivieren Sie das Kopieren von Daten in Data Editor für Nicht-Admin-Benutzer'], + ['data_editor_public_settings_disable_data_export_name', 'Disable Export'], + ['data_editor_public_settings_disable_data_export_description', 'Disable exporting of data in Data Editor for non-admin users'], + ['data_editor_public_settings_disable_data_import_name', 'Disable Import'], + ['data_editor_public_settings_disable_data_import_description', 'Disable importing of data in Data Editor for non-admin users'], +]; diff --git a/webapp/packages/plugin-data-editor-public-settings/src/locales/en.ts b/webapp/packages/plugin-data-editor-public-settings/src/locales/en.ts new file mode 100644 index 0000000000..45265be29d --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/locales/en.ts @@ -0,0 +1,10 @@ +export default [ + ['data_editor_public_settings_disable_edit_name', 'Disable Edit'], + ['data_editor_public_settings_disable_edit_description', 'Disable editing of data in Data Editor for non-admin users'], + ['data_editor_public_settings_disable_data_copy_name', 'Disable Copy'], + ['data_editor_public_settings_disable_data_copy_description', 'Disable copying of data in Data Editor for non-admin users'], + ['data_editor_public_settings_disable_data_export_name', 'Disable Export'], + ['data_editor_public_settings_disable_data_export_description', 'Disable exporting of data in Data Editor for non-admin users'], + ['data_editor_public_settings_disable_data_import_name', 'Disable Import'], + ['data_editor_public_settings_disable_data_import_description', 'Disable importing of data in Data Editor for non-admin users'], +]; diff --git a/webapp/packages/plugin-data-editor-public-settings/src/locales/fr.ts b/webapp/packages/plugin-data-editor-public-settings/src/locales/fr.ts new file mode 100644 index 0000000000..99204612bd --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/locales/fr.ts @@ -0,0 +1,16 @@ +export default [ + ['data_editor_public_settings_disable_edit_name', "Désactiver l'édition"], + [ + 'data_editor_public_settings_disable_edit_description', + "Désactiver l'édition des données dans le Data Editor pour les utilisateurs non administrateurs", + ], + ['data_editor_public_settings_disable_data_copy_name', 'Désactiver la copie'], + [ + 'data_editor_public_settings_disable_data_copy_description', + 'Désactiver la copie des données dans le Data Editor pour les utilisateurs non administrateurs', + ], + ['data_editor_public_settings_disable_data_export_name', 'Disable Export'], + ['data_editor_public_settings_disable_data_export_description', 'Disable exporting of data in Data Editor for non-admin users'], + ['data_editor_public_settings_disable_data_import_name', 'Disable Import'], + ['data_editor_public_settings_disable_data_import_description', 'Disable importing of data in Data Editor for non-admin users'], +]; diff --git a/webapp/packages/plugin-data-editor-public-settings/src/locales/it.ts b/webapp/packages/plugin-data-editor-public-settings/src/locales/it.ts new file mode 100644 index 0000000000..45265be29d --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/locales/it.ts @@ -0,0 +1,10 @@ +export default [ + ['data_editor_public_settings_disable_edit_name', 'Disable Edit'], + ['data_editor_public_settings_disable_edit_description', 'Disable editing of data in Data Editor for non-admin users'], + ['data_editor_public_settings_disable_data_copy_name', 'Disable Copy'], + ['data_editor_public_settings_disable_data_copy_description', 'Disable copying of data in Data Editor for non-admin users'], + ['data_editor_public_settings_disable_data_export_name', 'Disable Export'], + ['data_editor_public_settings_disable_data_export_description', 'Disable exporting of data in Data Editor for non-admin users'], + ['data_editor_public_settings_disable_data_import_name', 'Disable Import'], + ['data_editor_public_settings_disable_data_import_description', 'Disable importing of data in Data Editor for non-admin users'], +]; diff --git a/webapp/packages/plugin-data-editor-public-settings/src/locales/ru.ts b/webapp/packages/plugin-data-editor-public-settings/src/locales/ru.ts new file mode 100644 index 0000000000..4dfbafd60d --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/locales/ru.ts @@ -0,0 +1,10 @@ +export default [ + ['data_editor_public_settings_disable_edit_name', 'Отключить редактирование'], + ['data_editor_public_settings_disable_edit_description', 'Отключить редактирование данных для пользователей без прав администратора'], + ['data_editor_public_settings_disable_data_copy_name', 'Отключить копирование'], + ['data_editor_public_settings_disable_data_copy_description', 'Отключить копирование данных для пользователей без прав администратора'], + ['data_editor_public_settings_disable_data_export_name', 'Отключить скачивание данных'], + ['data_editor_public_settings_disable_data_export_description', 'Отключить скачивание данных для пользователей без прав администратора'], + ['data_editor_public_settings_disable_data_import_name', 'Отключить импорт данных'], + ['data_editor_public_settings_disable_data_import_description', 'Отключить импорт данных для пользователей без прав администратора'], +]; diff --git a/webapp/packages/plugin-data-editor-public-settings/src/locales/vi.ts b/webapp/packages/plugin-data-editor-public-settings/src/locales/vi.ts new file mode 100644 index 0000000000..4459235be9 --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/locales/vi.ts @@ -0,0 +1,22 @@ +export default [ + ['data_editor_public_settings_disable_edit_name', 'Tắt chỉnh sửa'], + [ + 'data_editor_public_settings_disable_edit_description', + 'Tắt chỉnh sửa dữ liệu trong Trình xem dữ liệu đối với người dùng không phải quản trị viên', + ], + ['data_editor_public_settings_disable_data_copy_name', 'Tắt sao chép'], + [ + 'data_editor_public_settings_disable_data_copy_description', + 'Tắt sao chép dữ liệu trong Trình xem dữ liệu đối với người dùng không phải quản trị viên', + ], + ['data_editor_public_settings_disable_data_export_name', 'Tắt xuất'], + [ + 'data_editor_public_settings_disable_data_export_description', + 'Tắt xuất dữ liệu trong Trình xem dữ liệu đối với người dùng không phải quản trị viên', + ], + ['data_editor_public_settings_disable_data_import_name', 'Tắt Nhập dữ liệu'], + [ + 'data_editor_public_settings_disable_data_import_description', + 'Tắt tính năng nhập dữ liệu trong Data Editor đối với người dùng không phải quản trị viên', + ], +]; diff --git a/webapp/packages/plugin-data-editor-public-settings/src/locales/zh.ts b/webapp/packages/plugin-data-editor-public-settings/src/locales/zh.ts new file mode 100644 index 0000000000..5eddc5fcd9 --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/locales/zh.ts @@ -0,0 +1,10 @@ +export default [ + ['data_editor_public_settings_disable_edit_name', '禁用编辑'], + ['data_editor_public_settings_disable_edit_description', '在数据查看器中为非管理员用户禁用数据编辑'], + ['data_editor_public_settings_disable_data_copy_name', '禁用复制'], + ['data_editor_public_settings_disable_data_copy_description', '在数据查看器中为非管理员用户禁用数据复制'], + ['data_editor_public_settings_disable_data_export_name', '禁用导出'], + ['data_editor_public_settings_disable_data_export_description', '在数据查看器中为非管理员用户禁用数据导出'], + ['data_editor_public_settings_disable_data_import_name', 'Disable Import'], + ['data_editor_public_settings_disable_data_import_description', 'Disable importing of data in Data Editor for non-admin users'], +]; diff --git a/webapp/packages/plugin-data-editor-public-settings/src/manifest.ts b/webapp/packages/plugin-data-editor-public-settings/src/manifest.ts new file mode 100644 index 0000000000..c714961b56 --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/manifest.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; + +export const pluginDataEditorPublicSettingsManifest: PluginManifest = { + info: { + name: 'Plugin data editor settings', + }, +}; diff --git a/webapp/packages/plugin-data-editor-public-settings/src/module.ts b/webapp/packages/plugin-data-editor-public-settings/src/module.ts new file mode 100644 index 0000000000..c18706c23d --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/src/module.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ModuleRegistry, Bootstrap } from '@cloudbeaver/core-di'; +import { PluginDataEditorPublicSettingsBootstrap } from './PluginDataEditorPublicSettingsBootstrap.js'; +import { LocaleService } from './LocaleService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-data-editor-public-settings', + + configure: serviceCollection => { + serviceCollection.addSingleton(Bootstrap, LocaleService).addSingleton(Bootstrap, PluginDataEditorPublicSettingsBootstrap); + }, +}); diff --git a/webapp/packages/plugin-data-editor-public-settings/tsconfig.json b/webapp/packages/plugin-data-editor-public-settings/tsconfig.json new file mode 100644 index 0000000000..40c0ca5b8a --- /dev/null +++ b/webapp/packages/plugin-data-editor-public-settings/tsconfig.json @@ -0,0 +1,52 @@ +{ + "extends": "@cloudbeaver/tsconfig/tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true + }, + "references": [ + { + "path": "../../common-typescript/@dbeaver/cli" + }, + { + "path": "../core-cli" + }, + { + "path": "../core-di" + }, + { + "path": "../core-di/tsconfig.json" + }, + { + "path": "../core-localization" + }, + { + "path": "../core-root" + }, + { + "path": "../core-settings" + }, + { + "path": "../core-utils" + }, + { + "path": "../plugin-data-import" + }, + { + "path": "../plugin-data-viewer" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*" + ] +} diff --git a/webapp/packages/plugin-data-export/package.json b/webapp/packages/plugin-data-export/package.json index 705478b9d4..27bb0d37a9 100644 --- a/webapp/packages/plugin-data-export/package.json +++ b/webapp/packages/plugin-data-export/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-data-export", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,42 +11,44 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "pretest": "tsc -b", - "test": "core-cli-test", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "test": "dbeaver-test", + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-data-viewer": "~0.1.0", - "@cloudbeaver/plugin-sql-editor": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x", - "@testing-library/jest-dom": "~6.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-navigation-tree": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-data-viewer": "workspace:*", + "@cloudbeaver/plugin-sql-editor": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, "devDependencies": { - "@cloudbeaver/tests-runner": "~0.1.0" + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@dbeaver/cli": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" } } diff --git a/webapp/packages/plugin-data-export/src/Bootstrap.ts b/webapp/packages/plugin-data-export/src/Bootstrap.ts deleted file mode 100644 index 182ca1ae8c..0000000000 --- a/webapp/packages/plugin-data-export/src/Bootstrap.ts +++ /dev/null @@ -1,23 +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 { Bootstrap as B, injectable } from '@cloudbeaver/core-di'; - -import { DataExportMenuService } from './DataExportMenuService'; - -@injectable() -export class Bootstrap extends B { - constructor(private readonly dataExportMenuService: DataExportMenuService) { - super(); - } - - register(): void { - this.dataExportMenuService.register(); - } - - load(): void {} -} diff --git a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts index 4dcec1dcf6..3a29aebcb8 100644 --- a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts @@ -1,114 +1,121 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createConnectionParam, DATA_CONTEXT_CONNECTION } from '@cloudbeaver/core-connections'; +import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, IMenuContext } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { LocalizationService } from '@cloudbeaver/core-localization'; -import { DATA_CONTEXT_NAV_NODE, EObjectFeature } from '@cloudbeaver/core-navigation-tree'; -import { ACTION_EXPORT, ActionService, DATA_CONTEXT_MENU_NESTED, MenuService } from '@cloudbeaver/core-view'; -import { IDatabaseDataSource, IDataContainerOptions, ITableFooterMenuContext, TableFooterMenuService } from '@cloudbeaver/plugin-data-viewer'; -import type { IDataQueryOptions } from '@cloudbeaver/plugin-sql-editor'; +import { ActionService, MenuService } from '@cloudbeaver/core-view'; +import { DataViewerService } from '@cloudbeaver/plugin-data-viewer'; -import { DataExportSettingsService } from './DataExportSettingsService'; -import { DataExportDialog } from './Dialog/DataExportDialog'; +// const DataExportDialog = importLazyComponent(() => import('./Dialog/DataExportDialog.js').then(module => module.DataExportDialog)); -@injectable() +@injectable(() => [CommonDialogService, ActionService, MenuService, LocalizationService, DataViewerService, ConnectionInfoResource]) export class DataExportMenuService { - constructor( - private readonly commonDialogService: CommonDialogService, - private readonly tableFooterMenuService: TableFooterMenuService, - private readonly dataExportSettingsService: DataExportSettingsService, - private readonly actionService: ActionService, - private readonly menuService: MenuService, - private readonly localizationService: LocalizationService, - ) {} + constructor() // private readonly actionService: ActionService, // private readonly commonDialogService: CommonDialogService, + // private readonly menuService: MenuService, + // private readonly localizationService: LocalizationService, + // private readonly dataViewerService: DataViewerService, + // private readonly connectionInfoResource: ConnectionInfoResource, + {} register(): void { - this.tableFooterMenuService.registerMenuItem({ - id: 'export ', - order: 5, - title: 'data_transfer_dialog_export', - tooltip: 'data_transfer_dialog_export_tooltip', - icon: 'table-export', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden: () => this.isDisabled(), - isDisabled(context) { - return ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.getResult(context.data.resultIndex) - ); - }, - onClick: this.exportData.bind(this), - }); - - this.menuService.addCreator({ - isApplicable: context => { - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); - - if (node && !node.objectFeatures.includes(EObjectFeature.dataContainer)) { - return false; - } - - return !this.isDisabled() && context.has(DATA_CONTEXT_CONNECTION) && !context.has(DATA_CONTEXT_MENU_NESTED); - }, - getItems: (context, items) => [...items, ACTION_EXPORT], - }); - - this.actionService.addHandler({ - id: 'data-export', - isActionApplicable: (context, action) => action === ACTION_EXPORT && context.has(DATA_CONTEXT_CONNECTION) && context.has(DATA_CONTEXT_NAV_NODE), - handler: async (context, action) => { - const node = context.get(DATA_CONTEXT_NAV_NODE); - const connection = context.get(DATA_CONTEXT_CONNECTION); - - this.commonDialogService.open(DataExportDialog, { - connectionKey: createConnectionParam(connection), - name: node?.name, - containerNodePath: node?.id, - }); - }, - }); - } - - private exportData(context: IMenuContext) { - const result = context.data.model.getResult(context.data.resultIndex); - - if (!result) { - throw new Error('Result must be provided'); - } - - const source = context.data.model.source as IDatabaseDataSource; - - if (!source.options) { - throw new Error('Source options must be provided'); - } - - this.commonDialogService.open(DataExportDialog, { - connectionKey: source.options.connectionKey, - contextId: context.data.model.source.executionContext?.context?.id, - containerNodePath: source.options.containerNodePath, - resultId: result.id, - name: context.data.model.name ?? undefined, - query: source.options.query, - filter: { - constraints: source.options.constraints, - where: source.options.whereFilter, - }, - }); - } - - private isDisabled() { - if (this.dataExportSettingsService.settings.isValueDefault('disabled')) { - return this.dataExportSettingsService.deprecatedSettings.getValue('disabled'); - } - return this.dataExportSettingsService.settings.getValue('disabled'); + // this.menuService.addCreator({ + // menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + // contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + // isApplicable: context => { + // const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION); + // return this.dataViewerService.canExportData && (!presentation || presentation.type === DataViewerPresentationType.Data); + // }, + // getItems(context, items) { + // return [...items, ACTION_EXPORT]; + // }, + // orderItems(context, items) { + // const extracted = menuExtractItems(items, [ACTION_EXPORT]); + // return [...items, ...extracted]; + // }, + // }); + // this.actionService.addHandler({ + // id: 'data-export-base-handler', + // menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + // contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + // isHidden: (context, action) => !this.dataViewerService.canExportData, + // actions: [ACTION_EXPORT], + // isActionApplicable: context => { + // const model = context.get(DATA_CONTEXT_DV_DDM)!; + // return isResultSetDataSource(model.source); + // }, + // isDisabled(context) { + // const model = context.get(DATA_CONTEXT_DV_DDM)!; + // const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + // return model.isLoading() || model.isDisabled(resultIndex) || !model.source.getResult(resultIndex); + // }, + // getActionInfo(context, action) { + // if (action === ACTION_EXPORT) { + // return { ...action.info, icon: 'table-export' }; + // } + // return action.info; + // }, + // handler: (context, action) => { + // const model = context.get(DATA_CONTEXT_DV_DDM)!; + // const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + // if (action === ACTION_EXPORT) { + // const result = model.source.getResult(resultIndex); + // const source = model.source; + // if (!result || !isResultSetDataSource(source)) { + // throw new Error('Result must be provided'); + // } + // if (!source.options) { + // throw new Error('Source options must be provided'); + // } + // this.commonDialogService.open(DataExportDialog, { + // connectionKey: source.options.connectionKey, + // contextId: source.executionContext?.context?.id, + // containerNodePath: source.options.containerNodePath, + // resultId: result.id, + // name: model.name ?? undefined, + // fileName: withTimestamp(model.name ?? this.localizationService.translate('data_transfer_dialog_title')), + // query: source.options.query, + // filter: { + // constraints: source.options.constraints, + // where: source.options.whereFilter, + // }, + // }); + // } + // }, + // }); + // this.menuService.addCreator({ + // root: true, + // contexts: [DATA_CONTEXT_NAV_NODE], + // isApplicable: context => { + // const node = context.get(DATA_CONTEXT_NAV_NODE)!; + // if (!node.objectFeatures.includes(EObjectFeature.dataContainer)) { + // return false; + // } + // return this.dataViewerService.canExportData && context.has(DATA_CONTEXT_CONNECTION); + // }, + // getItems: (context, items) => [...items, ACTION_EXPORT], + // }); + // this.actionService.addHandler({ + // id: 'data-export', + // actions: [ACTION_EXPORT], + // contexts: [DATA_CONTEXT_CONNECTION, DATA_CONTEXT_NAV_NODE], + // handler: async context => { + // const node = context.get(DATA_CONTEXT_NAV_NODE)!; + // const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; + // const connection = await this.connectionInfoResource.load(connectionKey); + // const fileName = withTimestamp(`${connection.name}${node.name ? ` - ${node.name}` : ''}`); + // this.commonDialogService.open(DataExportDialog, { + // connectionKey, + // name: node.name, + // fileName, + // containerNodePath: node.id, + // }); + // }, + // }); } } diff --git a/webapp/packages/plugin-data-export/src/DataExportProcessService.ts b/webapp/packages/plugin-data-export/src/DataExportProcessService.ts index 9d05adf8e5..30f4229ba8 100644 --- a/webapp/packages/plugin-data-export/src/DataExportProcessService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportProcessService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,12 +8,12 @@ import type { IConnectionInfoParams } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { DataTransferParameters, GraphQLService } from '@cloudbeaver/core-sdk'; +import { type DataTransferParameters, GraphQLService } from '@cloudbeaver/core-sdk'; import { Deferred, GlobalConstants, OrderedMap } from '@cloudbeaver/core-utils'; -import { ExportFromContainerProcess } from './ExportFromContainerProcess'; -import { ExportFromResultsProcess } from './ExportFromResultsProcess'; -import type { IExportContext } from './IExportContext'; +import { ExportFromContainerProcess } from './ExportFromContainerProcess.js'; +import { ExportFromResultsProcess } from './ExportFromResultsProcess.js'; +import type { IExportContext } from './IExportContext.js'; interface Process { taskId: string; @@ -27,13 +27,16 @@ export interface ExportProcess { process: Deferred; } -@injectable() +@injectable(() => [GraphQLService, NotificationService]) export class DataExportProcessService { readonly exportProcesses = new OrderedMap(value => value.taskId); - constructor(private readonly graphQLService: GraphQLService, private readonly notificationService: NotificationService) {} + constructor( + private readonly graphQLService: GraphQLService, + private readonly notificationService: NotificationService, + ) {} - async cancel(exportId: string): Promise { + cancel(exportId: string): void { const process = this.exportProcesses.get(exportId); if (!process) { return; @@ -41,21 +44,14 @@ export class DataExportProcessService { process.process.cancel(); } - async delete(exportId: string): Promise { + delete(exportId: string): void { const process = this.exportProcesses.get(exportId); + if (!process) { return; } - try { - const dataFileId = process.process.getPayload(); - if (dataFileId) { - await this.graphQLService.sdk.removeDataTransferFile({ dataFileId }); - } - } catch (exception: any) { - this.notificationService.logException(exception, 'Error occurred while deleting file'); - } finally { - this.exportProcesses.remove(exportId); - } + + this.exportProcesses.remove(exportId); } download(exportId: string): void { diff --git a/webapp/packages/plugin-data-export/src/DataExportService.ts b/webapp/packages/plugin-data-export/src/DataExportService.ts index 820a55ed0f..fd44064dd8 100644 --- a/webapp/packages/plugin-data-export/src/DataExportService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,27 +9,21 @@ import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import type { DataTransferParameters } from '@cloudbeaver/core-sdk'; -import { DataExportProcessService } from './DataExportProcessService'; -import { DataTransferProcessorsResource } from './DataTransferProcessorsResource'; -import { ExportNotification } from './ExportNotification/ExportNotification'; -import type { IExportContext } from './IExportContext'; +import { DataExportProcessService } from './DataExportProcessService.js'; +import { ExportNotification } from './ExportNotification/ExportNotification.js'; +import type { IExportContext } from './IExportContext.js'; -@injectable() +@injectable(() => [NotificationService, DataExportProcessService]) export class DataExportService { constructor( private readonly notificationService: NotificationService, private readonly dataExportProcessService: DataExportProcessService, - readonly processors: DataTransferProcessorsResource, ) {} async cancel(exportId: string): Promise { await this.dataExportProcessService.cancel(exportId); } - async delete(exportId: string): Promise { - await this.dataExportProcessService.delete(exportId); - } - download(exportId: string): void { this.dataExportProcessService.download(exportId); } diff --git a/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts b/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts deleted file mode 100644 index dd37fdf68f..0000000000 --- a/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts +++ /dev/null @@ -1,124 +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 '@testing-library/jest-dom'; - -import { coreAdministrationManifest } from '@cloudbeaver/core-administration'; -import { coreAppManifest } from '@cloudbeaver/core-app'; -import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; -import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; -import { coreBrowserManifest } from '@cloudbeaver/core-browser'; -import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; -import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; -import { coreEventsManifest } from '@cloudbeaver/core-events'; -import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; -import { coreProjectsManifest } from '@cloudbeaver/core-projects'; -import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; -import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; -import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; -import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; -import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; -import { coreRoutingManifest } from '@cloudbeaver/core-routing'; -import { coreSDKManifest } from '@cloudbeaver/core-sdk'; -import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { coreThemingManifest } from '@cloudbeaver/core-theming'; -import { coreUIManifest } from '@cloudbeaver/core-ui'; -import { coreViewManifest } from '@cloudbeaver/core-view'; -import { dataViewerManifest } from '@cloudbeaver/plugin-data-viewer'; -import { datasourceContextSwitchPluginManifest } from '@cloudbeaver/plugin-datasource-context-switch'; -import { navigationTabsPlugin } from '@cloudbeaver/plugin-navigation-tabs'; -import { navigationTreePlugin } from '@cloudbeaver/plugin-navigation-tree'; -import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; -import { createApp } from '@cloudbeaver/tests-runner'; - -import { DataExportSettings, DataExportSettingsService } from './DataExportSettingsService'; -import { dataExportManifest } from './manifest'; - -const endpoint = createGQLEndpoint(); -const app = createApp( - dataExportManifest, - coreLocalizationManifest, - coreEventsManifest, - corePluginManifest, - coreProductManifest, - coreRootManifest, - coreSDKManifest, - coreBrowserManifest, - coreSettingsManifest, - coreViewManifest, - coreAuthenticationManifest, - coreProjectsManifest, - coreUIManifest, - coreRoutingManifest, - coreAdministrationManifest, - coreConnectionsManifest, - coreDialogsManifest, - coreNavigationTree, - coreAppManifest, - coreThemingManifest, - datasourceContextSwitchPluginManifest, - navigationTreePlugin, - navigationTabsPlugin, - objectViewerManifest, - dataViewerManifest, -); - -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); - -beforeAll(() => app.init()); - -const testValueA = true; -const testValueB = true; - -const equalConfigA = { - plugin_data_export: { - disabled: testValueA, - } as DataExportSettings, - plugin: { - 'data-export': { - disabled: testValueA, - } as DataExportSettings, - }, -}; - -const equalConfigB = { - plugin_data_export: { - disabled: testValueB, - } as DataExportSettings, - plugin: { - 'data-export': { - disabled: testValueB, - } as DataExportSettings, - }, -}; - -test('New settings equal deprecated settings A', async () => { - const settings = app.injector.getServiceByClass(DataExportSettingsService); - const config = app.injector.getServiceByClass(ServerConfigResource); - - server.use(endpoint.query('serverConfig', mockServerConfig(equalConfigA))); - - await config.refresh(); - - expect(settings.settings.getValue('disabled')).toBe(testValueA); - expect(settings.deprecatedSettings.getValue('disabled')).toBe(testValueA); -}); - -test('New settings equal deprecated settings B', async () => { - const settings = app.injector.getServiceByClass(DataExportSettingsService); - const config = app.injector.getServiceByClass(ServerConfigResource); - - server.use(endpoint.query('serverConfig', mockServerConfig(equalConfigB))); - - await config.refresh(); - - expect(settings.settings.getValue('disabled')).toBe(testValueB); - expect(settings.deprecatedSettings.getValue('disabled')).toBe(testValueB); -}); diff --git a/webapp/packages/plugin-data-export/src/DataExportSettingsService.ts b/webapp/packages/plugin-data-export/src/DataExportSettingsService.ts deleted file mode 100644 index b9ce196529..0000000000 --- a/webapp/packages/plugin-data-export/src/DataExportSettingsService.ts +++ /dev/null @@ -1,27 +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 { injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; - -const defaultSettings = { - disabled: false, -}; - -export type DataExportSettings = typeof defaultSettings; - -@injectable() -export class DataExportSettingsService { - readonly settings: PluginSettings; - /** @deprecated Use settings instead, will be removed in 23.0.0 */ - readonly deprecatedSettings: PluginSettings; - - constructor(private readonly pluginManagerService: PluginManagerService) { - this.settings = this.pluginManagerService.createSettings('data-export', 'plugin', defaultSettings); - this.deprecatedSettings = this.pluginManagerService.getDeprecatedPluginSettings('plugin_data_export', defaultSettings); - } -} diff --git a/webapp/packages/plugin-data-export/src/DataTransferProcessorsResource.ts b/webapp/packages/plugin-data-export/src/DataTransferProcessorsResource.ts index 9601cc7acf..4eb0748b5b 100644 --- a/webapp/packages/plugin-data-export/src/DataTransferProcessorsResource.ts +++ b/webapp/packages/plugin-data-export/src/DataTransferProcessorsResource.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,11 +8,14 @@ import { injectable } from '@cloudbeaver/core-di'; import { CachedMapAllKey, CachedMapResource, resourceKeyList } from '@cloudbeaver/core-resource'; import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { DataTransferProcessorInfo, GraphQLService } from '@cloudbeaver/core-sdk'; +import { type DataTransferProcessorInfo, GraphQLService } from '@cloudbeaver/core-sdk'; -@injectable() +@injectable(() => [GraphQLService, ServerConfigResource]) export class DataTransferProcessorsResource extends CachedMapResource { - constructor(private readonly graphQLService: GraphQLService, serverConfigResource: ServerConfigResource) { + constructor( + private readonly graphQLService: GraphQLService, + serverConfigResource: ServerConfigResource, + ) { super(() => new Map()); this.sync( serverConfigResource, diff --git a/webapp/packages/plugin-data-export/src/Dialog/DataExportController.ts b/webapp/packages/plugin-data-export/src/Dialog/DataExportController.ts deleted file mode 100644 index 97002c9a21..0000000000 --- a/webapp/packages/plugin-data-export/src/Dialog/DataExportController.ts +++ /dev/null @@ -1,158 +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 { computed, makeObservable, observable } from 'mobx'; - -import { ErrorDetailsDialog, type IProperty } from '@cloudbeaver/core-blocks'; -import { IDestructibleController, IInitializableController, injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { DataTransferOutputSettings, DataTransferProcessorInfo, GQLErrorCatcher } from '@cloudbeaver/core-sdk'; - -import { DataExportService } from '../DataExportService'; -import type { IExportContext } from '../IExportContext'; -import { DefaultExportOutputSettingsResource } from './DefaultExportOutputSettingsResource'; - -export enum DataExportStep { - DataTransferProcessor, - Configure, -} - -@injectable() -export class DataExportController implements IInitializableController, IDestructibleController { - step = DataExportStep.DataTransferProcessor; - get isLoading(): boolean { - return this.dataExportService.processors.isLoading(); - } - - isExporting = false; - processor: DataTransferProcessorInfo | null = null; - - get processors(): DataTransferProcessorInfo[] { - return Array.from(this.dataExportService.processors.data.values()).sort(sortProcessors); - } - - processorProperties: any = {}; - properties: IProperty[] = []; - - outputSettings: Partial = {}; - - readonly error = new GQLErrorCatcher(); - - private context!: IExportContext; - private close!: () => void; - private isDistructed = false; - - constructor( - private readonly dataExportService: DataExportService, - private readonly notificationService: NotificationService, - private readonly commonDialogService: CommonDialogService, - private readonly defaultExportOutputSettingsResource: DefaultExportOutputSettingsResource, - ) { - makeObservable(this, { - step: observable, - isExporting: observable, - processor: observable, - processors: computed, - processorProperties: observable, - properties: observable, - outputSettings: observable, - }); - } - - init(context: IExportContext, close: () => void): void { - this.context = context; - this.close = close; - this.loadProcessors(); - this.loadDefaultOutputSettings(); - } - - destruct(): void { - this.isDistructed = true; - } - - prepareExport = async () => { - if (!this.processor || this.isExporting) { - return; - } - this.isExporting = true; - - try { - await this.dataExportService.exportData(this.context, { - processorId: this.processor.id, - processorProperties: this.processorProperties, - filter: this.context.filter, - outputSettings: this.outputSettings, - }); - this.close(); - } catch (exception: any) { - if (!this.error.catch(exception) || this.isDistructed) { - this.notificationService.logException(exception, "Can't export"); - } - } finally { - this.isExporting = false; - this.close(); - } - }; - - setStep = (step: DataExportStep) => { - this.step = step; - }; - - selectProcessor = (processorId: string) => { - this.processor = this.dataExportService.processors.data.get(processorId)!; - - this.properties = - this.processor.properties?.map(property => ({ - id: property.id!, - key: property.id!, - displayName: property.displayName, - description: property.description, - validValues: property.validValues, - defaultValue: property.defaultValue, - valuePlaceholder: property.defaultValue, - })) || []; - - this.processorProperties = {}; - - this.step = DataExportStep.Configure; - this.error.clear(); - }; - - showDetails = () => { - if (this.error.exception) { - this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); - } - }; - - private async loadProcessors() { - try { - await this.dataExportService.processors.load(); - } catch (exception: any) { - this.notificationService.logException(exception, "Can't load data export processors"); - } - } - - private async loadDefaultOutputSettings() { - try { - const data = await this.defaultExportOutputSettingsResource.load(); - if (data) { - Object.assign(this.outputSettings, data.outputSettings); - } - } catch (exception: any) { - this.notificationService.logException(exception, "Can't load output settings"); - } - } -} - -function sortProcessors(processorA: DataTransferProcessorInfo, processorB: DataTransferProcessorInfo): number { - if (processorA.order === processorB.order) { - return (processorA.name || '').localeCompare(processorB.name || ''); - } - - return processorA.order - processorB.order; -} diff --git a/webapp/packages/plugin-data-export/src/Dialog/DataExportDialog.tsx b/webapp/packages/plugin-data-export/src/Dialog/DataExportDialog.tsx index 4283b7d21d..374e14a8f4 100644 --- a/webapp/packages/plugin-data-export/src/Dialog/DataExportDialog.tsx +++ b/webapp/packages/plugin-data-export/src/Dialog/DataExportDialog.tsx @@ -1,47 +1,42 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useController } from '@cloudbeaver/core-di'; +import { useResource } from '@cloudbeaver/core-blocks'; import type { DialogComponent } from '@cloudbeaver/core-dialogs'; -import type { IExportContext } from '../IExportContext'; -import { DataExportController, DataExportStep } from './DataExportController'; -import { ProcessorConfigureDialog } from './ProcessorConfigureDialog'; -import { ProcessorSelectDialog } from './ProcessorSelectDialog'; +import type { IExportContext } from '../IExportContext.js'; +import { DefaultExportOutputSettingsResource } from './DefaultExportOutputSettingsResource.js'; +import { EDataExportStep } from './EDataExportStep.js'; +import { ProcessorConfigureDialog } from './ProcessorConfigureDialog.js'; +import { ProcessorSelectDialog } from './ProcessorSelectDialog.js'; +import { useDataExportDialog } from './useDataExportDialog.js'; export const DataExportDialog: DialogComponent = observer(function DataExportDialog({ payload, rejectDialog }) { - const controller = useController(DataExportController, payload, rejectDialog); + useResource(DataExportDialog, DefaultExportOutputSettingsResource, undefined, { forceSuspense: true }); - if (controller.step === DataExportStep.Configure && controller.processor) { + const dialog = useDataExportDialog(payload, rejectDialog); + + if (dialog.step === EDataExportStep.Configure && dialog.processor) { return ( controller.setStep(DataExportStep.DataTransferProcessor)} + processor={dialog.processor} + properties={dialog.properties} + processorProperties={dialog.processorProperties} + error={dialog.exception} + isExporting={dialog.processing} + outputSettings={dialog.outputSettings} + onBack={() => dialog.setStep(EDataExportStep.DataTransferProcessor)} onClose={rejectDialog} - onExport={controller.prepareExport} + onExport={dialog.export} /> ); } - return ( - - ); + return ; }); diff --git a/webapp/packages/plugin-data-export/src/Dialog/DefaultExportOutputSettingsResource.ts b/webapp/packages/plugin-data-export/src/Dialog/DefaultExportOutputSettingsResource.ts index 0077c40a2e..50bc2a5552 100644 --- a/webapp/packages/plugin-data-export/src/Dialog/DefaultExportOutputSettingsResource.ts +++ b/webapp/packages/plugin-data-export/src/Dialog/DefaultExportOutputSettingsResource.ts @@ -1,15 +1,15 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; import { CachedDataResource } from '@cloudbeaver/core-resource'; -import { DataTransferDefaultExportSettings, GraphQLService } from '@cloudbeaver/core-sdk'; +import { type DataTransferDefaultExportSettings, GraphQLService } from '@cloudbeaver/core-sdk'; -@injectable() +@injectable(() => [GraphQLService]) export class DefaultExportOutputSettingsResource extends CachedDataResource { constructor(private readonly graphQLService: GraphQLService) { super(() => null); diff --git a/webapp/packages/plugin-data-export/src/Dialog/EDataExportStep.ts b/webapp/packages/plugin-data-export/src/Dialog/EDataExportStep.ts new file mode 100644 index 0000000000..e9fabfd809 --- /dev/null +++ b/webapp/packages/plugin-data-export/src/Dialog/EDataExportStep.ts @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +export enum EDataExportStep { + DataTransferProcessor, + Configure, +} diff --git a/webapp/packages/plugin-data-export/src/Dialog/ExportProcessorList/ExportProcessorList.tsx b/webapp/packages/plugin-data-export/src/Dialog/ExportProcessorList/ExportProcessorList.tsx index 54d859e9ed..ed91f5c213 100644 --- a/webapp/packages/plugin-data-export/src/Dialog/ExportProcessorList/ExportProcessorList.tsx +++ b/webapp/packages/plugin-data-export/src/Dialog/ExportProcessorList/ExportProcessorList.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import { ItemList } from '@cloudbeaver/core-blocks'; import type { DataTransferProcessorInfo } from '@cloudbeaver/core-sdk'; -import { ProcessorItem } from './ProcessorItem'; +import { ProcessorItem } from './ProcessorItem.js'; interface Props { processors: DataTransferProcessorInfo[]; diff --git a/webapp/packages/plugin-data-export/src/Dialog/ExportProcessorList/ProcessorItem.module.css b/webapp/packages/plugin-data-export/src/Dialog/ExportProcessorList/ProcessorItem.module.css new file mode 100644 index 0000000000..caa9d53ead --- /dev/null +++ b/webapp/packages/plugin-data-export/src/Dialog/ExportProcessorList/ProcessorItem.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.staticImage { + box-sizing: border-box; + width: 24px; + max-height: 24px; +} diff --git a/webapp/packages/plugin-data-export/src/Dialog/ExportProcessorList/ProcessorItem.tsx b/webapp/packages/plugin-data-export/src/Dialog/ExportProcessorList/ProcessorItem.tsx index 37bd33c79c..e92523c69d 100644 --- a/webapp/packages/plugin-data-export/src/Dialog/ExportProcessorList/ProcessorItem.tsx +++ b/webapp/packages/plugin-data-export/src/Dialog/ExportProcessorList/ProcessorItem.tsx @@ -1,40 +1,33 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; import { useCallback } from 'react'; -import styled, { css } from 'reshadow'; import { ListItem, ListItemDescription, ListItemIcon, ListItemName, StaticImage } from '@cloudbeaver/core-blocks'; import type { DataTransferProcessorInfo } from '@cloudbeaver/core-sdk'; +import style from './ProcessorItem.module.css'; + interface Props { processor: DataTransferProcessorInfo; onSelect: (processorId: string) => void; } -const styles = css` - StaticImage { - box-sizing: border-box; - width: 24px; - max-height: 24px; - } -`; - export const ProcessorItem = observer(function ProcessorItem({ processor, onSelect }) { const select = useCallback(() => onSelect(processor.id), [processor]); - return styled(styles)( + return ( - + {processor.name} {processor.description} - , + ); }); diff --git a/webapp/packages/plugin-data-export/src/Dialog/OutputOptionsForm.tsx b/webapp/packages/plugin-data-export/src/Dialog/OutputOptionsForm.tsx index 064911cbec..43ac3870cf 100644 --- a/webapp/packages/plugin-data-export/src/Dialog/OutputOptionsForm.tsx +++ b/webapp/packages/plugin-data-export/src/Dialog/OutputOptionsForm.tsx @@ -1,23 +1,16 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled, { css } from 'reshadow'; import { Combobox, Container, FieldCheckbox, Loader, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import type { DataTransferOutputSettings } from '@cloudbeaver/core-sdk'; -import { DefaultExportOutputSettingsResource } from './DefaultExportOutputSettingsResource'; - -const styles = css` - Combobox { - width: 140px; - } -`; +import { DefaultExportOutputSettingsResource } from './DefaultExportOutputSettingsResource.js'; interface Props { outputSettings: Partial; @@ -25,7 +18,6 @@ interface Props { export const OutputOptionsForm = observer(function OutputOptionsForm(props: Props) { const translate = useTranslate(); - const resource = useResource(OutputOptionsForm, DefaultExportOutputSettingsResource, undefined); return ( @@ -37,10 +29,10 @@ export const OutputOptionsForm = observer(function OutputOptionsForm(props: Prop return null; } - return styled(styles)( + return ( - + Encoding @@ -52,7 +44,7 @@ export const OutputOptionsForm = observer(function OutputOptionsForm(props: Prop - , + ); }} diff --git a/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.m.css b/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.m.css deleted file mode 100644 index 9f1949f500..0000000000 --- a/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.m.css +++ /dev/null @@ -1,14 +0,0 @@ -.propertiesTable { - flex: 1; - overflow: hidden; - padding: 12px 0; -} -.errorMessage { - composes: theme-background-secondary theme-text-on-secondary from global; - position: sticky; - bottom: 0; - padding: 8px 24px; -} -.tabList { - margin: 0 10px; -} diff --git a/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.module.css b/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.module.css new file mode 100644 index 0000000000..306b22abcc --- /dev/null +++ b/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.module.css @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.container .propertiesTable { + flex: 1; + overflow: hidden; + padding: 12px 0; +} +.errorMessage { + composes: theme-background-secondary theme-text-on-secondary from global; + position: sticky; + bottom: 0; + padding: 8px 24px; +} +.tabList { + margin: 0 10px; +} diff --git a/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.tsx b/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.tsx index 7ed14bec05..3c4e3e6966 100644 --- a/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.tsx +++ b/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.tsx @@ -1,13 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; import { useState } from 'react'; -import styled from 'reshadow'; import { CommonDialogBody, @@ -15,28 +14,27 @@ import { CommonDialogHeader, CommonDialogWrapper, ErrorMessage, - IProperty, + type IProperty, PropertiesTable, s, + useErrorDetails, useS, - useStyles, useTranslate, } from '@cloudbeaver/core-blocks'; -import type { DataTransferOutputSettings, DataTransferProcessorInfo, GQLErrorCatcher } from '@cloudbeaver/core-sdk'; -import { ITabData, Tab, TabList, TabsState, TabTitle, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui'; +import type { DataTransferOutputSettings, DataTransferProcessorInfo } from '@cloudbeaver/core-sdk'; +import { type ITabData, Tab, TabList, TabsState, TabTitle } from '@cloudbeaver/core-ui'; -import { OutputOptionsForm } from './OutputOptionsForm'; -import style from './ProcessorConfigureDialog.m.css'; -import { ProcessorConfigureDialogFooter } from './ProcessorConfigureDialogFooter'; +import { OutputOptionsForm } from './OutputOptionsForm.js'; +import style from './ProcessorConfigureDialog.module.css'; +import { ProcessorConfigureDialogFooter } from './ProcessorConfigureDialogFooter.js'; interface Props { processor: DataTransferProcessorInfo; properties: IProperty[]; processorProperties: any; outputSettings: Partial; - error: GQLErrorCatcher; + error: Error | null; isExporting: boolean; - onShowDetails: () => void; onClose: () => void; onBack: () => void; onExport: () => void; @@ -54,7 +52,6 @@ export const ProcessorConfigureDialog = observer(function ProcessorConfig outputSettings, error, isExporting, - onShowDetails, onClose, onBack, onExport, @@ -64,6 +61,7 @@ export const ProcessorConfigureDialog = observer(function ProcessorConfig const title = `${translate('data_transfer_dialog_configuration_title')} (${processor.name})`; const [currentTabId, setCurrentTabId] = useState(SETTINGS_TABS.EXTRACTION); + const errorDetails = useErrorDetails(error); function handleTabChange(tab: ITabData) { setCurrentTabId(tab.tabId as SETTINGS_TABS); @@ -81,17 +79,17 @@ export const ProcessorConfigureDialog = observer(function ProcessorConfig } } - return styled(useStyles(UNDERLINE_TAB_STYLES))( - + return ( + {!processor.isBinary ? ( - - + + {translate('data_transfer_format_settings')} - + {translate('data_transfer_output_settings')} @@ -103,12 +101,12 @@ export const ProcessorConfigureDialog = observer(function ProcessorConfig )} - {error.responseMessage && ( + {error && ( )} @@ -122,6 +120,6 @@ export const ProcessorConfigureDialog = observer(function ProcessorConfig onNext={handleNextClick} /> - , + ); }); diff --git a/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialogFooter.module.css b/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialogFooter.module.css new file mode 100644 index 0000000000..9f7bb0f7c6 --- /dev/null +++ b/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialogFooter.module.css @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.controls { + display: flex; + flex: 1; + height: 100%; + align-items: center; + margin: auto; + gap: 24px; +} diff --git a/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialogFooter.tsx b/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialogFooter.tsx index 0f92303f8e..32fcf81d22 100644 --- a/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialogFooter.tsx +++ b/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialogFooter.tsx @@ -1,29 +1,15 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled, { css } from 'reshadow'; -import { Button, useTranslate } from '@cloudbeaver/core-blocks'; +import { Button, Fill, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; -const styles = css` - controls { - display: flex; - flex: 1; - height: 100%; - align-items: center; - margin: auto; - gap: 24px; - } - - fill { - flex: 1; - } -`; +import style from './ProcessorConfigureDialogFooter.module.css'; interface Props { isExporting: boolean; @@ -43,25 +29,26 @@ export const ProcessorConfigureDialogFooter = observer(function Processor onNext, }) { const translate = useTranslate(); + const styles = useS(style); - return styled(styles)( - - - - {isFinalStep ? ( - ) : ( - )} - , + ); }); diff --git a/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.m.css b/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.m.css deleted file mode 100644 index 7507cef88e..0000000000 --- a/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.m.css +++ /dev/null @@ -1,21 +0,0 @@ -.exportProcessorList { - flex: 1; -} -.exportObject { - composes: theme-typography--body2 from global; - flex-shrink: 0; - padding: 16px 24px; - padding-top: 0; - max-height: 50px; - overflow: hidden; - - & pre { - margin: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: initial; - } -} -.pre { - composes: theme-typography--caption from global; -} diff --git a/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.module.css b/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.module.css new file mode 100644 index 0000000000..f3bf76a8a9 --- /dev/null +++ b/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.module.css @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.exportProcessorList { + flex: 1; +} +.exportObject { + composes: theme-typography--body2 from global; + flex-shrink: 0; + padding: 16px 24px; + padding-top: 0; + max-height: 50px; + overflow: hidden; + + & pre { + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: initial; + } +} +.pre { + composes: theme-typography--caption from global; +} diff --git a/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.tsx b/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.tsx index 0f5a986c55..4c761e152d 100644 --- a/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.tsx +++ b/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.tsx @@ -1,29 +1,34 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { CommonDialogBody, CommonDialogHeader, CommonDialogWrapper, Loader, s, useS } from '@cloudbeaver/core-blocks'; +import { CommonDialogBody, CommonDialogHeader, CommonDialogWrapper, s, useResource, useS } from '@cloudbeaver/core-blocks'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import type { DataTransferProcessorInfo } from '@cloudbeaver/core-sdk'; -import type { IExportContext } from '../IExportContext'; -import { ExportProcessorList } from './ExportProcessorList/ExportProcessorList'; -import style from './ProcessorSelectDialog.m.css'; +import { DataTransferProcessorsResource } from '../DataTransferProcessorsResource.js'; +import type { IExportContext } from '../IExportContext.js'; +import { ExportProcessorList } from './ExportProcessorList/ExportProcessorList.js'; +import style from './ProcessorSelectDialog.module.css'; interface Props { context: IExportContext; - processors: DataTransferProcessorInfo[]; - isLoading: boolean; onSelect: (processorId: string) => void; onClose: () => void; } -export const ProcessorSelectDialog = observer(function ProcessorSelectDialog({ context, processors, isLoading, onSelect, onClose }) { +export const ProcessorSelectDialog = observer(function ProcessorSelectDialog({ context, onSelect, onClose }) { const styles = useS(style); + const dataTransferProcessorsResource = useResource(ProcessorSelectDialog, DataTransferProcessorsResource, CachedMapAllKey, { + forceSuspense: true, + }); + + const processors = dataTransferProcessorsResource.resource.values.slice().sort(sortProcessors); return ( @@ -36,9 +41,16 @@ export const ProcessorSelectDialog = observer(function ProcessorSelectDia )} - {isLoading && } - {!isLoading && } + ); }); + +function sortProcessors(processorA: DataTransferProcessorInfo, processorB: DataTransferProcessorInfo): number { + if (processorA.order === processorB.order) { + return (processorA.name || '').localeCompare(processorB.name || ''); + } + + return processorA.order - processorB.order; +} diff --git a/webapp/packages/plugin-data-export/src/Dialog/useDataExportDialog.ts b/webapp/packages/plugin-data-export/src/Dialog/useDataExportDialog.ts new file mode 100644 index 0000000000..dfbf66d5b1 --- /dev/null +++ b/webapp/packages/plugin-data-export/src/Dialog/useDataExportDialog.ts @@ -0,0 +1,135 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { action, computed, observable, toJS } from 'mobx'; + +import { type IProperty, useObservableRef } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import type { DataTransferOutputSettings, DataTransferProcessorInfo } from '@cloudbeaver/core-sdk'; + +import { DataExportService } from '../DataExportService.js'; +import { DataTransferProcessorsResource } from '../DataTransferProcessorsResource.js'; +import type { IExportContext } from '../IExportContext.js'; +import { DefaultExportOutputSettingsResource } from './DefaultExportOutputSettingsResource.js'; +import { EDataExportStep } from './EDataExportStep.js'; + +interface State { + readonly properties: IProperty[]; + step: EDataExportStep; + processor: DataTransferProcessorInfo | null; + processorProperties: Record; + outputSettings: Partial; + processing: boolean; + exception: Error | null; + setStep(step: EDataExportStep): void; + selectProcessor(processorId: string): Promise; + export(): Promise; +} + +export function useDataExportDialog(context: IExportContext, onExport?: () => void) { + const notificationService = useService(NotificationService); + const localizationService = useService(LocalizationService); + const dataExportService = useService(DataExportService); + const defaultExportOutputSettingsResource = useService(DefaultExportOutputSettingsResource); + const dataTransferProcessorsResource = useService(DataTransferProcessorsResource); + + const state: State = useObservableRef( + () => ({ + get properties() { + if (!this.processor?.properties) { + return []; + } + + return this.processor.properties.map(property => ({ + id: property.id!, + key: property.id!, + displayName: property.displayName, + description: property.description, + validValues: property.validValues, + defaultValue: property.defaultValue, + valuePlaceholder: property.defaultValue, + })); + }, + step: EDataExportStep.DataTransferProcessor, + processing: false, + processor: null as DataTransferProcessorInfo | null, + processorProperties: {}, + outputSettings: {}, + exception: null, + setStep(step: EDataExportStep) { + this.step = step; + }, + async selectProcessor(processorId: string) { + try { + this.processor = await this.dataTransferProcessorsResource.load(processorId); + const outputData = await this.defaultExportOutputSettingsResource.load(); + + if (outputData) { + this.outputSettings = toJS(outputData.outputSettings); + } + + this.processorProperties = {}; + this.setStep(EDataExportStep.Configure); + this.exception = null; + } catch (exception: any) { + this.notificationService.logException(exception, this.localizationService.translate('data_transfer_dialog_select_processor_fail')); + } + }, + async export() { + if (!this.processor || this.processing) { + return; + } + + this.processing = true; + this.exception = null; + + try { + await this.dataExportService.exportData(this.context, { + processorId: this.processor.id, + processorProperties: this.processorProperties, + filter: this.context.filter, + outputSettings: { + ...this.outputSettings, + fileName: this.context.fileName, + }, + }); + + this.onExport?.(); + } catch (exception: any) { + this.exception = exception; + } finally { + this.processing = false; + } + }, + }), + { + processorProperties: observable, + outputSettings: observable, + processor: observable.ref, + step: observable.ref, + exception: observable.ref, + processing: observable.ref, + properties: computed, + setStep: action.bound, + export: action.bound, + selectProcessor: action.bound, + }, + { + context, + onExport, + notificationService, + dataExportService, + localizationService, + defaultExportOutputSettingsResource, + dataTransferProcessorsResource, + }, + ); + + return state; +} diff --git a/webapp/packages/plugin-data-export/src/ExportFromContainerProcess.ts b/webapp/packages/plugin-data-export/src/ExportFromContainerProcess.ts index b80333445f..23621d9949 100644 --- a/webapp/packages/plugin-data-export/src/ExportFromContainerProcess.ts +++ b/webapp/packages/plugin-data-export/src/ExportFromContainerProcess.ts @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { IConnectionInfoParams } from '@cloudbeaver/core-connections'; import type { NotificationService } from '@cloudbeaver/core-events'; -import { AsyncTaskInfo, DataTransferParameters, GraphQLService, ServerInternalError } from '@cloudbeaver/core-sdk'; +import { type AsyncTaskInfo, type DataTransferParameters, GraphQLService, ServerInternalError } from '@cloudbeaver/core-sdk'; import { CancellablePromise, cancellableTimeout, Deferred, EDeferredState } from '@cloudbeaver/core-utils'; const DELAY_BETWEEN_TRIES = 1000; @@ -18,7 +18,10 @@ export class ExportFromContainerProcess extends Deferred { private timeout?: CancellablePromise; private isCancelConfirmed = false; // true when server successfully executed cancelQueryAsync - constructor(private readonly graphQLService: GraphQLService, private readonly notificationService: NotificationService) { + constructor( + private readonly graphQLService: GraphQLService, + private readonly notificationService: NotificationService, + ) { super(); } @@ -50,7 +53,7 @@ export class ExportFromContainerProcess extends Deferred { * this method just mark process as cancelling * to avoid racing conditions the server request will be executed in synchronous manner in start method */ - cancel() { + override cancel() { if (this.getState() !== EDeferredState.PENDING) { return; } diff --git a/webapp/packages/plugin-data-export/src/ExportFromResultsProcess.ts b/webapp/packages/plugin-data-export/src/ExportFromResultsProcess.ts index 7d8ae9bca5..7865a2607c 100644 --- a/webapp/packages/plugin-data-export/src/ExportFromResultsProcess.ts +++ b/webapp/packages/plugin-data-export/src/ExportFromResultsProcess.ts @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { IConnectionInfoParams } from '@cloudbeaver/core-connections'; import type { NotificationService } from '@cloudbeaver/core-events'; -import { AsyncTaskInfo, DataTransferParameters, GraphQLService, ServerInternalError } from '@cloudbeaver/core-sdk'; +import { type AsyncTaskInfo, type DataTransferParameters, GraphQLService, ServerInternalError } from '@cloudbeaver/core-sdk'; import { CancellablePromise, cancellableTimeout, Deferred, EDeferredState } from '@cloudbeaver/core-utils'; const DELAY_BETWEEN_TRIES = 1000; @@ -18,7 +18,10 @@ export class ExportFromResultsProcess extends Deferred { private timeout?: CancellablePromise; private isCancelConfirmed = false; // true when server successfully executed cancelQueryAsync - constructor(private readonly graphQLService: GraphQLService, private readonly notificationService: NotificationService) { + constructor( + private readonly graphQLService: GraphQLService, + private readonly notificationService: NotificationService, + ) { super(); } @@ -51,7 +54,7 @@ export class ExportFromResultsProcess extends Deferred { * this method just mark process as cancelling * to avoid racing conditions the server request will be executed in synchronous manner in start method */ - cancel() { + override cancel() { if (this.getState() !== EDeferredState.PENDING) { return; } diff --git a/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotification.module.css b/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotification.module.css new file mode 100644 index 0000000000..037c7e1511 --- /dev/null +++ b/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotification.module.css @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.message { + composes: theme-typography--body1 from global; + opacity: 0.8; + overflow: auto; + max-height: 100px; + word-break: break-word; + white-space: pre-line; +} + +.subText { + composes: theme-typography--body2 from global; +} + +.sourceName { + padding-top: 16px; + max-height: 50px; + overflow: hidden; +} + +.pre { + margin: 0; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} diff --git a/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotification.tsx b/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotification.tsx index f9cbe8fa5e..32b58362cb 100644 --- a/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotification.tsx +++ b/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotification.tsx @@ -1,91 +1,84 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled, { css } from 'reshadow'; -import { Button, SnackbarBody, SnackbarContent, SnackbarFooter, SnackbarStatus, SnackbarWrapper, useTranslate } from '@cloudbeaver/core-blocks'; -import { useController } from '@cloudbeaver/core-di'; -import { ENotificationType, NotificationComponentProps } from '@cloudbeaver/core-events'; +import { + Button, + s, + SnackbarBody, + SnackbarContent, + SnackbarFooter, + SnackbarStatus, + SnackbarWrapper, + Text, + useErrorDetails, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { ENotificationType } from '@cloudbeaver/core-events'; import { EDeferredState } from '@cloudbeaver/core-utils'; -import { ExportNotificationController } from './ExportNotificationController'; +import styles from './ExportNotification.module.css'; +import type { IExportNotification } from './IExportNotification.js'; +import { useExportNotification } from './useExportNotification.js'; -const styles = css` - message { - composes: theme-typography--body1 from global; - opacity: 0.8; - overflow: auto; - max-height: 100px; - word-break: break-word; - white-space: pre-line; - } - source-name { - composes: theme-typography--body2 from global; - padding-top: 16px; - max-height: 50px; - overflow: hidden; - - & pre { - margin: 0; - text-overflow: ellipsis; - overflow: hidden; - } - } -`; - -type Props = NotificationComponentProps<{ - source: string; -}>; +interface Props { + notification: IExportNotification; +} export const ExportNotification = observer(function ExportNotification({ notification }) { - const controller = useController(ExportNotificationController, notification); const translate = useTranslate(); - const { title, status, message } = controller.status; + const style = useS(styles); + const state = useExportNotification(notification); + const errorDetails = useErrorDetails(state.task?.process.getRejectionReason() ?? null); + + const { title, status, message } = state.status; + const isReadyToDownload = status === ENotificationType.Info && !!state.downloadUrl; - return styled(styles)( - + return ( + - {message && {message}} - - {controller.sourceName} - {controller.task?.context.query &&
{controller.task.context.query}
} -
+ {isReadyToDownload && {translate('plugin_data_export_download_process_info')}} + {message &&
{message}
} +
+ {state.sourceName} + {state.task?.context.query && ( +
+                {state.task.context.query}
+              
+ )} +
- {status === ENotificationType.Info && controller.downloadUrl && ( - <> - - - + )} {status === ENotificationType.Error && ( - )} {status === ENotificationType.Loading && ( - )}
-
, +
); }); diff --git a/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotificationController.ts b/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotificationController.ts deleted file mode 100644 index 0a2b452496..0000000000 --- a/webapp/packages/plugin-data-export/src/ExportNotification/ExportNotificationController.ts +++ /dev/null @@ -1,136 +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 { computed, makeObservable, observable } from 'mobx'; - -import { ErrorDetailsDialog } from '@cloudbeaver/core-blocks'; -import { IInitializableController, injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; -import { ENotificationType, INotification } from '@cloudbeaver/core-events'; -import { LocalizationService } from '@cloudbeaver/core-localization'; -import { NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; -import { ServerErrorType, ServerInternalError } from '@cloudbeaver/core-sdk'; -import { Deferred, EDeferredState, errorOf } from '@cloudbeaver/core-utils'; - -import { DataExportProcessService, ExportProcess } from '../DataExportProcessService'; - -interface ExportNotificationStatus { - title: string; - status: ENotificationType; - message?: string; -} -@injectable() -export class ExportNotificationController implements IInitializableController { - isDetailsDialogOpen = false; - - get isSuccess(): boolean { - return this.process?.getState() === EDeferredState.RESOLVED; - } - - get isPending(): boolean { - return !!this.process?.isInProgress; - } - - get process(): Deferred | undefined { - return this.task?.process; - } - - get task(): ExportProcess | undefined { - return this.dataExportProcessService.exportProcesses.get(this.notification.extraProps.source); - } - - get hasDetails(): boolean { - return !!this.process?.getRejectionReason(); - } - - get sourceName(): string { - if (!this.task) { - return ''; - } - - if (this.task.context.name) { - return this.task.context.name; - } - - return this.localization.translate('data_transfer_exporting_sql'); - } - - get status(): ExportNotificationStatus { - switch (this.process?.getState()) { - case EDeferredState.PENDING: - return { title: 'data_transfer_notification_preparation', status: ENotificationType.Loading }; - case EDeferredState.CANCELLING: - return { title: 'ui_processing_canceling', status: ENotificationType.Loading }; - case EDeferredState.RESOLVED: - return { title: 'data_transfer_notification_ready', status: ENotificationType.Info }; - case EDeferredState.CANCELLED: - return { title: 'data_transfer_notification_cancelled', status: ENotificationType.Info }; - default: { - const error = this.process?.getRejectionReason(); - - let title = 'data_transfer_notification_error'; - let status = ENotificationType.Error; - let message = ''; - - const serverInternalError = errorOf(error, ServerInternalError); - if (serverInternalError && serverInternalError.errorType === ServerErrorType.QUOTE_EXCEEDED) { - title = 'app_root_quota_exceeded'; - status = ENotificationType.Info; - message = serverInternalError.message; - } - - return { title, status, message }; - } - } - } - - get downloadUrl(): string | undefined { - return this.dataExportProcessService.downloadUrl(this.notification.extraProps.source); - } - - private notification!: INotification<{ source: string }>; - - constructor( - private readonly commonDialogService: CommonDialogService, - private readonly dataExportProcessService: DataExportProcessService, - private readonly navNodeManagerService: NavNodeManagerService, - private readonly localization: LocalizationService, - ) { - makeObservable(this, { - isDetailsDialogOpen: observable, - sourceName: computed, - }); - } - - init(notification: INotification<{ source: string }>): void { - this.notification = notification; - } - - delete = (): void => { - this.dataExportProcessService.delete(this.notification.extraProps.source); - this.notification.close(false); - }; - - download = (): void => { - this.dataExportProcessService.download(this.notification.extraProps.source); - this.notification.close(false); - }; - - cancel = (): void => { - this.dataExportProcessService.cancel(this.notification.extraProps.source); - }; - - showDetails = async (): Promise => { - this.isDetailsDialogOpen = true; - try { - this.notification.showDetails(); - await this.commonDialogService.open(ErrorDetailsDialog, this.process?.getRejectionReason()); - } finally { - this.isDetailsDialogOpen = false; - } - }; -} diff --git a/webapp/packages/plugin-data-export/src/ExportNotification/IExportNotification.ts b/webapp/packages/plugin-data-export/src/ExportNotification/IExportNotification.ts new file mode 100644 index 0000000000..4bc7277aac --- /dev/null +++ b/webapp/packages/plugin-data-export/src/ExportNotification/IExportNotification.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { ENotificationType, INotification } from '@cloudbeaver/core-events'; + +export type IExportNotification = INotification<{ source: string }>; + +export interface IExportNotificationStatus { + title: string; + status: ENotificationType; + message?: string; +} diff --git a/webapp/packages/plugin-data-export/src/ExportNotification/useExportNotification.ts b/webapp/packages/plugin-data-export/src/ExportNotification/useExportNotification.ts new file mode 100644 index 0000000000..0f1bf25230 --- /dev/null +++ b/webapp/packages/plugin-data-export/src/ExportNotification/useExportNotification.ts @@ -0,0 +1,101 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { action, computed } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { ENotificationType } from '@cloudbeaver/core-events'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { ServerErrorType, ServerInternalError } from '@cloudbeaver/core-sdk'; +import { EDeferredState, errorOf } from '@cloudbeaver/core-utils'; + +import { DataExportProcessService } from '../DataExportProcessService.js'; +import type { IExportNotification, IExportNotificationStatus } from './IExportNotification.js'; + +export function useExportNotification(notification: IExportNotification) { + const dataExportProcessService = useService(DataExportProcessService); + const localizationService = useService(LocalizationService); + + const state = useObservableRef( + () => ({ + get task() { + return this.dataExportProcessService.exportProcesses.get(this.notification.extraProps.source); + }, + get resolved() { + return this.task?.process.getState() === EDeferredState.RESOLVED; + }, + get sourceName() { + if (!this.task) { + return ''; + } + + if (this.task.context.name) { + return this.task.context.name; + } + + return this.localizationService.translate('data_transfer_exporting_sql'); + }, + get downloadUrl() { + return this.dataExportProcessService.downloadUrl(this.notification.extraProps.source); + }, + get status(): IExportNotificationStatus { + const process = this.task?.process; + + switch (process?.getState()) { + case EDeferredState.PENDING: + return { title: 'data_transfer_notification_preparation', status: ENotificationType.Loading }; + case EDeferredState.CANCELLING: + return { title: 'ui_processing_canceling', status: ENotificationType.Loading }; + case EDeferredState.RESOLVED: + return { title: 'data_transfer_notification_ready', status: ENotificationType.Info }; + case EDeferredState.CANCELLED: + return { title: 'data_transfer_notification_cancelled', status: ENotificationType.Info }; + default: { + const error = process?.getRejectionReason(); + const serverInternalError = errorOf(error, ServerInternalError); + + let title = 'data_transfer_notification_error'; + let status = ENotificationType.Error; + let message = ''; + + if (serverInternalError && serverInternalError.errorType === ServerErrorType.QUOTE_EXCEEDED) { + title = 'app_root_quota_exceeded'; + status = ENotificationType.Info; + message = serverInternalError.message; + } + + return { title, status, message }; + } + } + }, + delete() { + this.dataExportProcessService.delete(this.notification.extraProps.source); + this.notification.close(false); + }, + download() { + this.dataExportProcessService.download(this.notification.extraProps.source); + this.notification.close(false); + }, + cancel() { + this.dataExportProcessService.cancel(this.notification.extraProps.source); + }, + }), + { + resolved: computed, + task: computed, + sourceName: computed, + status: computed, + delete: action.bound, + download: action.bound, + cancel: action.bound, + }, + { notification, dataExportProcessService, localizationService }, + ); + + return state; +} diff --git a/webapp/packages/plugin-data-export/src/IExportContext.ts b/webapp/packages/plugin-data-export/src/IExportContext.ts index a0bdc2f209..0f56f4b0bb 100644 --- a/webapp/packages/plugin-data-export/src/IExportContext.ts +++ b/webapp/packages/plugin-data-export/src/IExportContext.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ export interface IExportContext { resultId?: string | null; containerNodePath?: string; name?: string; + fileName?: string; query?: string; filter?: SqlDataFilter; } diff --git a/webapp/packages/plugin-data-export/src/LocaleService.ts b/webapp/packages/plugin-data-export/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-data-export/src/LocaleService.ts +++ b/webapp/packages/plugin-data-export/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-data-export/src/PluginBootstrap.ts b/webapp/packages/plugin-data-export/src/PluginBootstrap.ts new file mode 100644 index 0000000000..74a7efd9f7 --- /dev/null +++ b/webapp/packages/plugin-data-export/src/PluginBootstrap.ts @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { DataExportMenuService } from './DataExportMenuService.js'; + +@injectable(() => [DataExportMenuService]) +export class PluginBootstrap extends Bootstrap { + constructor(private readonly dataExportMenuService: DataExportMenuService) { + super(); + } + + override register(): void { + this.dataExportMenuService.register(); + } +} diff --git a/webapp/packages/plugin-data-export/src/index.ts b/webapp/packages/plugin-data-export/src/index.ts index e3b911ba54..38d918f515 100644 --- a/webapp/packages/plugin-data-export/src/index.ts +++ b/webapp/packages/plugin-data-export/src/index.ts @@ -1,4 +1,13 @@ -export * from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 * from './DataExportMenuService'; -export * from './DataExportService'; +import './module.js'; +export * from './manifest.js'; + +export * from './DataExportMenuService.js'; +export * from './DataExportService.js'; diff --git a/webapp/packages/plugin-data-export/src/locales/en.ts b/webapp/packages/plugin-data-export/src/locales/en.ts index 46a1479da1..84b91de262 100644 --- a/webapp/packages/plugin-data-export/src/locales/en.ts +++ b/webapp/packages/plugin-data-export/src/locales/en.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_data_export_data_export_settings_group', 'Data export'], ['data_transfer_dialog_title', 'Export data'], ['data_transfer_dialog_export', 'Export'], ['data_transfer_dialog_export_tooltip', 'Export result set as a file'], @@ -13,7 +14,9 @@ export default [ ['data_transfer_exporting_table', 'Table:'], ['data_transfer_exporting_sql', 'SQL:'], ['data_transfer_format_settings', 'Format'], + ['data_transfer_dialog_select_processor_fail', 'Failed to select processor'], ['data_transfer_output_settings', 'Output'], ['data_transfer_output_settings_compress', 'Compression'], + ['plugin_data_export_download_process_info', 'The download process may take some time to start'], ]; diff --git a/webapp/packages/plugin-data-export/src/locales/fr.ts b/webapp/packages/plugin-data-export/src/locales/fr.ts new file mode 100644 index 0000000000..e74b92e89c --- /dev/null +++ b/webapp/packages/plugin-data-export/src/locales/fr.ts @@ -0,0 +1,21 @@ +export default [ + ['plugin_data_export_data_export_settings_group', 'Exportation des données'], + ['data_transfer_dialog_title', 'Exporter les données'], + ['data_transfer_dialog_export', 'Exporter'], + ['data_transfer_dialog_export_tooltip', 'Exporter le jeu de résultats en tant que fichier'], + ['data_transfer_dialog_configuration_title', "Configuration de l'exportation"], + ['data_transfer_dialog_preparation', "Nous préparons votre fichier pour l'exportation. Veuillez patienter..."], + ['data_transfer_notification_preparation', 'Nous préparons votre fichier pour le téléchargement. Veuillez patienter...'], + ['data_transfer_notification_ready', 'Le fichier est prêt à être téléchargé'], + ['data_transfer_notification_error', "L'exportation des données a échoué"], + ['data_transfer_notification_cancelled', "L'exportation des données a été annulée"], + ['data_transfer_notification_download', 'Télécharger'], + ['data_transfer_notification_delete', 'Supprimer'], + ['data_transfer_exporting_table', 'Table :'], + ['data_transfer_exporting_sql', 'SQL :'], + ['data_transfer_format_settings', 'Format'], + ['data_transfer_dialog_select_processor_fail', 'Échec de la sélection du processeur'], + ['data_transfer_output_settings', 'Sortie'], + ['data_transfer_output_settings_compress', 'Compression'], + ['plugin_data_export_download_process_info', 'The download process may take some time to start'], +]; diff --git a/webapp/packages/plugin-data-export/src/locales/it.ts b/webapp/packages/plugin-data-export/src/locales/it.ts index 9bd5c5619a..76b517640e 100644 --- a/webapp/packages/plugin-data-export/src/locales/it.ts +++ b/webapp/packages/plugin-data-export/src/locales/it.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_data_export_data_export_settings_group', 'Data export'], ['data_transfer_dialog_title', 'Esporta i dati'], ['data_transfer_dialog_export', 'Esporta'], ['data_transfer_dialog_export_tooltip', 'Esporta i risultati in un file'], @@ -11,6 +12,8 @@ export default [ ['data_transfer_notification_delete', 'Elimina'], ['data_transfer_exporting_table', 'Tabella:'], ['data_transfer_exporting_sql', 'SQL:'], + ['data_transfer_dialog_select_processor_fail', 'Failed to select processor'], ['data_transfer_output_settings_compress', 'Compression'], + ['plugin_data_export_download_process_info', 'The download process may take some time to start'], ]; diff --git a/webapp/packages/plugin-data-export/src/locales/ru.ts b/webapp/packages/plugin-data-export/src/locales/ru.ts index 053270ad89..393a5c326a 100644 --- a/webapp/packages/plugin-data-export/src/locales/ru.ts +++ b/webapp/packages/plugin-data-export/src/locales/ru.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_data_export_data_export_settings_group', 'Экспорт данных'], ['data_transfer_dialog_title', 'Экспорт данных'], ['data_transfer_dialog_export', 'Экспортировать'], ['data_transfer_dialog_export_tooltip', 'Экспортировать резалт сет как файл'], @@ -12,6 +13,8 @@ export default [ ['data_transfer_notification_delete', 'Удалить'], ['data_transfer_exporting_table', 'Таблица:'], ['data_transfer_exporting_sql', 'SQL:'], + ['data_transfer_dialog_select_processor_fail', 'Не удалось выбрать обработчик данных'], ['data_transfer_output_settings_compress', 'Сжатие'], + ['plugin_data_export_download_process_info', 'Начало загрузки может занять некоторое время'], ]; diff --git a/webapp/packages/plugin-data-export/src/locales/vi.ts b/webapp/packages/plugin-data-export/src/locales/vi.ts new file mode 100644 index 0000000000..6e44734dc3 --- /dev/null +++ b/webapp/packages/plugin-data-export/src/locales/vi.ts @@ -0,0 +1,22 @@ +export default [ + ['plugin_data_export_data_export_settings_group', 'Xuất dữ liệu'], + ['data_transfer_dialog_title', 'Xuất dữ liệu'], + ['data_transfer_dialog_export', 'Xuất'], + ['data_transfer_dialog_export_tooltip', 'Xuất tập hợp kết quả dưới dạng tệp'], + ['data_transfer_dialog_configuration_title', 'Cấu hình xuất'], + ['data_transfer_dialog_preparation', 'Chúng tôi đang chuẩn bị tệp của bạn để xuất. Vui lòng đợi...'], + ['data_transfer_notification_preparation', 'Chúng tôi đang chuẩn bị tệp của bạn để tải xuống. Vui lòng đợi...'], + ['data_transfer_notification_ready', 'Tệp đã sẵn sàng để tải xuống'], + ['data_transfer_notification_error', 'Xuất dữ liệu thất bại'], + ['data_transfer_notification_cancelled', 'Xuất dữ liệu đã bị hủy'], + ['data_transfer_notification_download', 'Tải xuống'], + ['data_transfer_notification_delete', 'Xóa'], + ['data_transfer_exporting_table', 'Bảng:'], + ['data_transfer_exporting_sql', 'SQL:'], + ['data_transfer_format_settings', 'Định dạng'], + ['data_transfer_dialog_select_processor_fail', 'Không thể chọn bộ xử lý'], + + ['data_transfer_output_settings', 'Đầu ra'], + ['data_transfer_output_settings_compress', 'Nén'], + ['plugin_data_export_download_process_info', 'Quá trình tải xuống có thể mất một chút thời gian để bắt đầu'], +]; diff --git a/webapp/packages/plugin-data-export/src/locales/zh.ts b/webapp/packages/plugin-data-export/src/locales/zh.ts index d01228165f..2701dfcac0 100644 --- a/webapp/packages/plugin-data-export/src/locales/zh.ts +++ b/webapp/packages/plugin-data-export/src/locales/zh.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_data_export_data_export_settings_group', '数据导出'], ['data_transfer_dialog_title', '导出数据'], ['data_transfer_dialog_export', '导出'], ['data_transfer_dialog_export_tooltip', '将结果集导出为文件'], @@ -12,6 +13,7 @@ export default [ ['data_transfer_notification_delete', '删除'], ['data_transfer_exporting_table', '表:'], ['data_transfer_exporting_sql', 'SQL:'], + ['data_transfer_dialog_select_processor_fail', '选择处理器失败'], - ['data_transfer_output_settings_compress', 'Compression'], + ['data_transfer_output_settings_compress', '压缩'], ]; diff --git a/webapp/packages/plugin-data-export/src/manifest.ts b/webapp/packages/plugin-data-export/src/manifest.ts index a110f606d3..b868c91416 100644 --- a/webapp/packages/plugin-data-export/src/manifest.ts +++ b/webapp/packages/plugin-data-export/src/manifest.ts @@ -1,34 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { Bootstrap } from './Bootstrap'; -import { DataExportMenuService } from './DataExportMenuService'; -import { DataExportProcessService } from './DataExportProcessService'; -import { DataExportService } from './DataExportService'; -import { DataExportSettingsService } from './DataExportSettingsService'; -import { DataTransferProcessorsResource } from './DataTransferProcessorsResource'; -import { DefaultExportOutputSettingsResource } from './Dialog/DefaultExportOutputSettingsResource'; -import { LocaleService } from './LocaleService'; - export const dataExportManifest: PluginManifest = { info: { name: 'Data Export Plugin', }, - - providers: [ - Bootstrap, - DataExportMenuService, - DataExportSettingsService, - DataExportService, - DataExportProcessService, - DataTransferProcessorsResource, - LocaleService, - DefaultExportOutputSettingsResource, - ], }; diff --git a/webapp/packages/plugin-data-export/src/module.ts b/webapp/packages/plugin-data-export/src/module.ts new file mode 100644 index 0000000000..db38daa967 --- /dev/null +++ b/webapp/packages/plugin-data-export/src/module.ts @@ -0,0 +1,33 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Dependency, ModuleRegistry, proxy, Bootstrap } from '@cloudbeaver/core-di'; +import { LocaleService } from './LocaleService.js'; +import { DefaultExportOutputSettingsResource } from './Dialog/DefaultExportOutputSettingsResource.js'; +import { PluginBootstrap } from './PluginBootstrap.js'; +import { DataTransferProcessorsResource } from './DataTransferProcessorsResource.js'; +import { DataExportService } from './DataExportService.js'; +import { DataExportProcessService } from './DataExportProcessService.js'; +import { DataExportMenuService } from './DataExportMenuService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-data-export', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Dependency, proxy(DefaultExportOutputSettingsResource)) + .addSingleton(Dependency, proxy(DataTransferProcessorsResource)) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(Bootstrap, PluginBootstrap) + .addSingleton(DataExportMenuService) + .addSingleton(DefaultExportOutputSettingsResource) + .addSingleton(DataTransferProcessorsResource) + .addSingleton(DataExportService) + .addSingleton(DataExportProcessService); + }, +}); diff --git a/webapp/packages/plugin-data-export/tsconfig.json b/webapp/packages/plugin-data-export/tsconfig.json index 4c44c4f4c9..aa77d441c7 100644 --- a/webapp/packages/plugin-data-export/tsconfig.json +++ b/webapp/packages/plugin-data-export/tsconfig.json @@ -1,61 +1,62 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-data-viewer/tsconfig.json" + "path": "../../common-typescript/@dbeaver/cli" }, { - "path": "../plugin-sql-editor/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-connections" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-navigation-tree/tsconfig.json" + "path": "../core-navigation-tree" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-sdk" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-utils" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-view" }, { - "path": "../core-view/tsconfig.json" + "path": "../plugin-data-viewer" }, { - "path": "../tests-runner/tsconfig.json" + "path": "../plugin-sql-editor" } ], "include": [ @@ -67,7 +68,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-data-grid/.gitignore b/webapp/packages/plugin-data-grid/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-data-grid/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-data-grid/package.json b/webapp/packages/plugin-data-grid/package.json new file mode 100644 index 0000000000..6561137087 --- /dev/null +++ b/webapp/packages/plugin-data-grid/package.json @@ -0,0 +1,44 @@ +{ + "name": "@cloudbeaver/plugin-data-grid", + "type": "module", + "sideEffects": [ + "./lib/module.js", + "./lib/index.js", + "src/**/*.css", + "src/**/*.scss", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "exports": { + ".": "./lib/index.js" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf --glob lib", + "lint": "eslint ./src/ --ext .ts,.tsx", + "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", + "test": "dbeaver-test", + "validate-dependencies": "core-cli-validate-dependencies" + }, + "dependencies": { + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@dbeaver/react-data-grid": "workspace:*", + "@dbeaver/ui-kit": "workspace:^", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" + }, + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@dbeaver/cli": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } +} diff --git a/webapp/packages/plugin-data-grid/src/DataGrid.module.css b/webapp/packages/plugin-data-grid/src/DataGrid.module.css new file mode 100644 index 0000000000..7be394abfa --- /dev/null +++ b/webapp/packages/plugin-data-grid/src/DataGrid.module.css @@ -0,0 +1,50 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.dataGrid { + --rdg-color: var(--theme-text-on-surface) !important; + --rdg-background-color: var(--theme-surface) !important; + --rdg-border-color: var(--theme-background) !important; + --rdg-header-background-color: var(--theme-surface) !important; + --rdg-font-size: inherit !important; + --rdg-row-hover-background-color: var(--theme-sub-secondary) !important; + --rdg-cell-frozen-box-shadow: none !important; + --rdg-selection-color: #0091ea !important; + + outline: 0 !important; + height: 100% !important; + border: none !important; + color-scheme: inherit !important; + + :global(.rdg-cell) { + outline: 0 !important; + + &[aria-selected='true'] { + box-shadow: inset 0 0 0 1px #808080; + } + } + + :global(.rdg-cell-editing) { + overflow: visible; + height: 24px; + box-shadow: none !important; + } + + :global(.rdg-editor-container) { + position: relative; + display: flex; + width: 100%; + height: 100%; + } +} + +.dataGrid:focus-within { + :global(.rdg-cell[aria-selected='true']) { + box-shadow: inset 0 0 0 1px var(--rdg-selection-color) !important; + } +} diff --git a/webapp/packages/plugin-data-grid/src/DataGrid.tsx b/webapp/packages/plugin-data-grid/src/DataGrid.tsx new file mode 100644 index 0000000000..804b9544a7 --- /dev/null +++ b/webapp/packages/plugin-data-grid/src/DataGrid.tsx @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { clsx } from '@dbeaver/ui-kit'; +import { observer } from 'mobx-react-lite'; +import { forwardRef } from 'react'; + +import { DataGrid as BaseDataGrid } from '@dbeaver/react-data-grid'; + +import classes from './DataGrid.module.css'; + +export const DataGrid = observer( + forwardRef(function DataGrid(props, ref) { + return ; + }), +) as typeof BaseDataGrid; diff --git a/webapp/packages/plugin-data-grid/src/DataGridLazy.ts b/webapp/packages/plugin-data-grid/src/DataGridLazy.ts new file mode 100644 index 0000000000..8eaa434dd9 --- /dev/null +++ b/webapp/packages/plugin-data-grid/src/DataGridLazy.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const DataGrid = importLazyComponent(() => import('./DataGrid.js').then(m => m.DataGrid)); diff --git a/webapp/packages/plugin-data-grid/src/PluginBootstrap.ts b/webapp/packages/plugin-data-grid/src/PluginBootstrap.ts new file mode 100644 index 0000000000..09687f9836 --- /dev/null +++ b/webapp/packages/plugin-data-grid/src/PluginBootstrap.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; + +@injectable() +export class PluginBootstrap extends Bootstrap { + constructor() { + super(); + } +} diff --git a/webapp/packages/plugin-data-grid/src/index.ts b/webapp/packages/plugin-data-grid/src/index.ts new file mode 100644 index 0000000000..c0c76be6ec --- /dev/null +++ b/webapp/packages/plugin-data-grid/src/index.ts @@ -0,0 +1,31 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { dataGridPlugin } from './manifest.js'; + +export default dataGridPlugin; +export { dataGridPlugin }; + +export { DataGrid } from './DataGridLazy.js'; + +export { + DataGridCellInnerContext, + useCreateGridReactiveValue, + BooleanFormatter, + DateFormatter, + NullFormatter, + NumberFormatter, + BlobFormatter, + type IGridReactiveValue, + type DataGridRef, + type ICellPosition, + type IDataGridCellRenderer, + type IDataGridCellProps, + type DataGridProps, +} from '@dbeaver/react-data-grid'; diff --git a/webapp/packages/plugin-data-grid/src/manifest.ts b/webapp/packages/plugin-data-grid/src/manifest.ts new file mode 100644 index 0000000000..d87212d020 --- /dev/null +++ b/webapp/packages/plugin-data-grid/src/manifest.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; + +export const dataGridPlugin: PluginManifest = { + info: { name: 'Data grid plugin' }, +}; diff --git a/webapp/packages/plugin-data-grid/src/module.ts b/webapp/packages/plugin-data-grid/src/module.ts new file mode 100644 index 0000000000..f95275c06b --- /dev/null +++ b/webapp/packages/plugin-data-grid/src/module.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ModuleRegistry, Bootstrap } from '@cloudbeaver/core-di'; +import { PluginBootstrap } from './PluginBootstrap.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-data-grid', + + configure: serviceCollection => { + serviceCollection.addSingleton(Bootstrap, PluginBootstrap); + }, +}); diff --git a/webapp/packages/plugin-data-grid/tsconfig.json b/webapp/packages/plugin-data-grid/tsconfig.json new file mode 100644 index 0000000000..56544be455 --- /dev/null +++ b/webapp/packages/plugin-data-grid/tsconfig.json @@ -0,0 +1,40 @@ +{ + "extends": "@cloudbeaver/tsconfig/tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true + }, + "references": [ + { + "path": "../../common-react/@dbeaver/react-data-grid" + }, + { + "path": "../../common-react/@dbeaver/ui-kit" + }, + { + "path": "../../common-typescript/@dbeaver/cli" + }, + { + "path": "../core-blocks" + }, + { + "path": "../core-cli" + }, + { + "path": "../core-di" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*" + ] +} diff --git a/webapp/packages/plugin-data-import/.gitignore b/webapp/packages/plugin-data-import/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-data-import/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-data-import/package.json b/webapp/packages/plugin-data-import/package.json new file mode 100644 index 0000000000..8666e8bc04 --- /dev/null +++ b/webapp/packages/plugin-data-import/package.json @@ -0,0 +1,52 @@ +{ + "name": "@cloudbeaver/plugin-data-import", + "type": "module", + "sideEffects": [ + "./lib/module.js", + "./lib/index.js", + "src/**/*.css", + "src/**/*.scss", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "exports": { + ".": "./lib/index.js" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf --glob lib", + "lint": "eslint ./src/ --ext .ts,.tsx", + "test": "dbeaver-test", + "validate-dependencies": "core-cli-validate-dependencies" + }, + "dependencies": { + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-settings": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-data-export": "workspace:*", + "@cloudbeaver/plugin-data-viewer": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" + }, + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@dbeaver/cli": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } +} diff --git a/webapp/packages/plugin-data-import/public/icons/data-import.svg b/webapp/packages/plugin-data-import/public/icons/data-import.svg new file mode 100644 index 0000000000..5fa8bd2841 --- /dev/null +++ b/webapp/packages/plugin-data-import/public/icons/data-import.svg @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-data-import/public/icons/data-import_m.svg b/webapp/packages/plugin-data-import/public/icons/data-import_m.svg new file mode 100644 index 0000000000..ca1b92e769 --- /dev/null +++ b/webapp/packages/plugin-data-import/public/icons/data-import_m.svg @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-data-import/public/icons/data-import_sm.svg b/webapp/packages/plugin-data-import/public/icons/data-import_sm.svg new file mode 100644 index 0000000000..d634271a82 --- /dev/null +++ b/webapp/packages/plugin-data-import/public/icons/data-import_sm.svg @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-data-import/src/DataImportBootstrap.ts b/webapp/packages/plugin-data-import/src/DataImportBootstrap.ts new file mode 100644 index 0000000000..33f0f05194 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportBootstrap.ts @@ -0,0 +1,122 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { ACTION_IMPORT, ActionService, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; +import { + ContainerDataSource, + DATA_CONTEXT_DV_DDM, + DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_PRESENTATION, + DATA_VIEWER_DATA_MODEL_ACTIONS_MENU, + DataViewerPresentationType, + isResultSetDataModel, +} from '@cloudbeaver/plugin-data-viewer'; + +import { DataImportDialogLazy } from './DataImportDialog/DataImportDialogLazy.js'; +import { DataImportService } from './DataImportService.js'; + +@injectable(() => [MenuService, ActionService, CommonDialogService, DataImportService]) +export class DataImportBootstrap extends Bootstrap { + constructor( + private readonly menuService: MenuService, + private readonly actionService: ActionService, + private readonly commonDialogService: CommonDialogService, + private readonly dataImportService: DataImportService, + ) { + super(); + } + + override register() { + this.actionService.addHandler({ + id: 'data-import-base-handler', + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + actions: [ACTION_IMPORT], + isActionApplicable: context => isResultSetDataModel(context.get(DATA_CONTEXT_DV_DDM)), + isDisabled(context) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + return model.isLoading() || model.isDisabled(resultIndex) || !model.source.getResult(resultIndex); + }, + getActionInfo(_, action) { + if (action === ACTION_IMPORT) { + return { ...action.info, icon: '/icons/data-import.svg' }; + } + + return action.info; + }, + handler: async (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)! as any; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + if (!isResultSetDataModel(model)) { + throw new Error('Execution context is not provided'); + } + + if (action === ACTION_IMPORT) { + const result = model.source.getResult(resultIndex); + + if (!result?.id) { + throw new Error('Result must be provided'); + } + + const executionContext = model.source.executionContext?.context; + + if (!executionContext) { + throw new Error('Execution context must be provided'); + } + + const state = await this.commonDialogService.open(DataImportDialogLazy, { tableName: model.name ?? model.id }); + + if (state === DialogueStateResult.Rejected || state === DialogueStateResult.Resolved) { + return; + } + + const success = await this.dataImportService.importData( + executionContext.connectionId, + executionContext.id, + executionContext.projectId, + result.id, + state.processorId, + state.file, + ); + + if (success) { + await model.refresh(); + } + } + }, + }); + + this.menuService.addCreator({ + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isApplicable: context => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION); + const isContainer = model.source instanceof ContainerDataSource; + return ( + !model.isReadonly(resultIndex) && + isContainer && + !this.dataImportService.disabled && + !presentation?.readonly && + (!presentation || presentation.type === DataViewerPresentationType.Data) + ); + }, + getItems(_, items) { + return [...items, ACTION_IMPORT]; + }, + orderItems(_, items) { + const extracted = menuExtractItems(items, [ACTION_IMPORT]); + return [...items, ...extracted]; + }, + }); + } +} diff --git a/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportDialog.tsx b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportDialog.tsx new file mode 100644 index 0000000000..a01b2e265d --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportDialog.tsx @@ -0,0 +1,84 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { + Button, + CommonDialogBody, + CommonDialogFooter, + CommonDialogHeader, + CommonDialogWrapper, + Container, + Fill, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import type { DialogComponent } from '@cloudbeaver/core-dialogs'; + +import { DataImportFileSelector } from './DataImportFileSelector.js'; +import { EDataImportDialogStep } from './EDataImportDialogStep.js'; +import type { IDataImportDialogState } from './IDataImportDialogState.js'; +import { ImportProcessorList } from './ImportProcessorList.js'; +import { useDataImportDialog } from './useDataImportDialog.js'; + +export interface IDataImportDialogResult { + file: File; + processorId: string; +} + +export interface IDataImportDialogPayload { + tableName: string; + initialState?: IDataImportDialogState; +} + +export const DataImportDialog: DialogComponent = observer(function DataImportDialog({ + payload, + resolveDialog, + rejectDialog, +}) { + const translate = useTranslate(); + const dialog = useDataImportDialog(payload.initialState); + + let title = translate('plugin_data_import_title'); + let icon = '/icons/data-import.svg'; + + if (dialog.state.step === EDataImportDialogStep.File && dialog.state.selectedProcessor) { + title += ` (${dialog.state.selectedProcessor.name ?? dialog.state.selectedProcessor.id})`; + icon = dialog.state.selectedProcessor.icon ?? icon; + } + + return ( + + + + {dialog.state.step === EDataImportDialogStep.Processor && } + {dialog.state.step === EDataImportDialogStep.File && } + + + + + + {dialog.state.step === EDataImportDialogStep.File && ( + + + + + )} + + + ); +}); diff --git a/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportDialogLazy.ts b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportDialogLazy.ts new file mode 100644 index 0000000000..766ec1a6e6 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportDialogLazy.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const DataImportDialogLazy = importLazyComponent(() => import('./DataImportDialog.js').then(m => m.DataImportDialog)); diff --git a/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileItem.module.css b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileItem.module.css new file mode 100644 index 0000000000..55d70afa22 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileItem.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tableColumnValue { + height: 36px; + padding: 0 24px; +} diff --git a/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileItem.tsx b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileItem.tsx new file mode 100644 index 0000000000..754946477e --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileItem.tsx @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { ActionIconButton, Container, s, TableColumnValue, TableItem, useS } from '@cloudbeaver/core-blocks'; + +import classes from './DataImportFileItem.module.css'; + +interface Props { + id: string; + name: string; + disabled?: boolean; + tooltip?: string; + className?: string; + onDelete: (id: string) => void; +} + +export const DataImportFileItem = observer(function DataImportFileItem({ id, name, tooltip, disabled, className, onDelete }) { + const styles = useS(classes); + + return ( + + {name} + + + onDelete(id)} /> + + + ); +}); diff --git a/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileSelector.module.css b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileSelector.module.css new file mode 100644 index 0000000000..86224cdd0c --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileSelector.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.columnHeader { + padding-left: 24px; + padding-right: 24px; +} diff --git a/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileSelector.tsx b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileSelector.tsx new file mode 100644 index 0000000000..d73f8ddb53 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportDialog/DataImportFileSelector.tsx @@ -0,0 +1,51 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Container, Group, InputFiles, s, Table, TableBody, TableColumnHeader, TableHeader, useS, useTranslate } from '@cloudbeaver/core-blocks'; + +import { DataImportFileItem } from './DataImportFileItem.js'; +import classes from './DataImportFileSelector.module.css'; +import type { IDataImportDialogState } from './IDataImportDialogState.js'; + +interface Props { + state: IDataImportDialogState; + onDelete: () => void; +} + +export const DataImportFileSelector = observer(function DataImportFileSelector({ state, onDelete }) { + const translate = useTranslate(); + const style = useS(classes); + + function handleFileSelect(value: FileList | null) { + if (value) { + state.file = value[0]!; + } + } + + const extension = state.selectedProcessor?.fileExtension ? `.${state.selectedProcessor.fileExtension}` : undefined; + + return ( + + + + + {translate('ui_name')} + + + + + + + + {state.file && } + +
+
+ ); +}); diff --git a/webapp/packages/plugin-data-import/src/DataImportDialog/EDataImportDialogStep.ts b/webapp/packages/plugin-data-import/src/DataImportDialog/EDataImportDialogStep.ts new file mode 100644 index 0000000000..e4385692bd --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportDialog/EDataImportDialogStep.ts @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +export enum EDataImportDialogStep { + Processor, + File, +} diff --git a/webapp/packages/plugin-data-import/src/DataImportDialog/IDataImportDialogState.ts b/webapp/packages/plugin-data-import/src/DataImportDialog/IDataImportDialogState.ts new file mode 100644 index 0000000000..2631efb81c --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportDialog/IDataImportDialogState.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { DataTransferProcessorInfo } from '@cloudbeaver/core-sdk'; + +import type { EDataImportDialogStep } from './EDataImportDialogStep.js'; + +export interface IDataImportDialogState { + step: EDataImportDialogStep; + file: File | null; + selectedProcessor: DataTransferProcessorInfo | null; +} diff --git a/webapp/packages/plugin-data-import/src/DataImportDialog/ImportProcessorList.module.css b/webapp/packages/plugin-data-import/src/DataImportDialog/ImportProcessorList.module.css new file mode 100644 index 0000000000..bd43f5c8d6 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportDialog/ImportProcessorList.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.staticImage { + display: flex; + width: 24px; +} diff --git a/webapp/packages/plugin-data-import/src/DataImportDialog/ImportProcessorList.tsx b/webapp/packages/plugin-data-import/src/DataImportDialog/ImportProcessorList.tsx new file mode 100644 index 0000000000..bde989dcce --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportDialog/ImportProcessorList.tsx @@ -0,0 +1,38 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { ItemList, ListItem, ListItemDescription, ListItemIcon, ListItemName, StaticImage, useResource } from '@cloudbeaver/core-blocks'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import type { DataTransferProcessorInfo } from '@cloudbeaver/core-sdk'; + +import { DataImportProcessorsResource } from '../DataImportProcessorsResource.js'; +import classes from './ImportProcessorList.module.css'; + +interface Props { + onSelect: (processor: DataTransferProcessorInfo) => void; + className?: string; +} + +export const ImportProcessorList = observer(function ImportProcessorList({ onSelect, className }) { + const dataImportProcessorsResource = useResource(ImportProcessorList, DataImportProcessorsResource, CachedMapAllKey, { forceSuspense: true }); + + return ( + + {dataImportProcessorsResource.resource.values.map(processor => ( + onSelect(processor)}> + + + + {processor.name} + {processor.description} + + ))} + + ); +}); diff --git a/webapp/packages/plugin-data-import/src/DataImportDialog/useDataImportDialog.ts b/webapp/packages/plugin-data-import/src/DataImportDialog/useDataImportDialog.ts new file mode 100644 index 0000000000..6ad0c6a587 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportDialog/useDataImportDialog.ts @@ -0,0 +1,59 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { action, observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import type { DataTransferProcessorInfo } from '@cloudbeaver/core-sdk'; + +import { EDataImportDialogStep } from './EDataImportDialogStep.js'; +import type { IDataImportDialogState } from './IDataImportDialogState.js'; + +interface IDialog { + state: IDataImportDialogState; + stepBack: () => void; + selectProcessor: (processor: DataTransferProcessorInfo) => void; + deleteFile: () => void; + reset: () => void; +} + +const DEFAULT_STATE_GETTER: () => IDataImportDialogState = () => ({ + step: EDataImportDialogStep.Processor, + file: null, + selectedProcessor: null, +}); + +export function useDataImportDialog(initialState?: IDataImportDialogState) { + const dialog = useObservableRef( + () => ({ + state: initialState ?? DEFAULT_STATE_GETTER(), + stepBack() { + if (this.state.step === EDataImportDialogStep.File) { + this.state.step = EDataImportDialogStep.Processor; + } + }, + selectProcessor(processor: DataTransferProcessorInfo) { + if (this.state.selectedProcessor && this.state.selectedProcessor.id !== processor.id) { + this.reset(); + } + + this.state.selectedProcessor = processor; + this.state.step = EDataImportDialogStep.File; + }, + deleteFile() { + this.state.file = null; + }, + reset() { + this.state = DEFAULT_STATE_GETTER(); + }, + }), + { state: observable, stepBack: action.bound, selectProcessor: action.bound, deleteFile: action.bound, reset: action.bound }, + false, + ); + + return dialog; +} diff --git a/webapp/packages/plugin-data-import/src/DataImportProcessorsResource.ts b/webapp/packages/plugin-data-import/src/DataImportProcessorsResource.ts new file mode 100644 index 0000000000..c7b244084d --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportProcessorsResource.ts @@ -0,0 +1,38 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { CachedMapAllKey, CachedMapResource, resourceKeyList } from '@cloudbeaver/core-resource'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { type DataTransferProcessorInfo, GraphQLService } from '@cloudbeaver/core-sdk'; + +@injectable(() => [GraphQLService, ServerConfigResource]) +export class DataImportProcessorsResource extends CachedMapResource { + constructor( + private readonly graphQLService: GraphQLService, + serverConfigResource: ServerConfigResource, + ) { + super(() => new Map()); + this.sync( + serverConfigResource, + () => {}, + () => CachedMapAllKey, + ); + } + + protected async loader(): Promise> { + const { processors } = await this.graphQLService.sdk.getDataTransferImportProcessors(); + + this.replace(resourceKeyList(processors.map(processor => processor.id)), processors); + + return this.data; + } + + protected validateKey(key: string): boolean { + return typeof key === 'string'; + } +} diff --git a/webapp/packages/plugin-data-import/src/DataImportService.ts b/webapp/packages/plugin-data-import/src/DataImportService.ts new file mode 100644 index 0000000000..71912e015e --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportService.ts @@ -0,0 +1,107 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ProcessSnackbar } from '@cloudbeaver/core-blocks'; +import { injectable } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { AsyncTaskInfoService, EAdminPermission, SessionPermissionsResource } from '@cloudbeaver/core-root'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; +import { getProgressPercent } from '@cloudbeaver/core-utils'; + +import { DataImportSettingsService } from './DataImportSettingsService.js'; + +@injectable(() => [DataImportSettingsService, NotificationService, GraphQLService, AsyncTaskInfoService, SessionPermissionsResource]) +export class DataImportService { + get disabled(): boolean { + if (this.sessionPermissionsResource.has(EAdminPermission.admin)) { + return false; + } + + return this.dataImportSettingsService.disabled; + } + + constructor( + private readonly dataImportSettingsService: DataImportSettingsService, + private readonly notificationService: NotificationService, + private readonly graphQLService: GraphQLService, + private readonly asyncTaskInfoService: AsyncTaskInfoService, + private readonly sessionPermissionsResource: SessionPermissionsResource, + ) { + makeObservable(this, { + disabled: computed, + }); + } + + async importData(connectionId: string, contextId: string, projectId: string, resultsId: string, processorId: string, file: File) { + const abortController = new AbortController(); + let cancelImplementation: (() => void | Promise) | null; + let isCancelled = false; + + function cancel() { + if (!cancelImplementation) { + return; + } + + cancelImplementation(); + isCancelled = true; + } + + const { controller, notification } = this.notificationService.processNotification( + () => ProcessSnackbar, + { + onCancel: cancel, + }, + { title: 'plugin_data_import_process_title', message: file.name, onClose: cancel }, + ); + + try { + cancelImplementation = () => abortController.abort(); + + const result = await this.graphQLService.sdk.uploadResultData( + connectionId, + contextId, + projectId, + resultsId, + processorId, + file, + event => { + if (isCancelled) { + return; + } + + if (event.total !== undefined) { + const percentCompleted = getProgressPercent(event.loaded, event.total); + + if (notification.message) { + controller.setMessage(`${percentCompleted}%\n${notification.message}`); + } + } + }, + abortController.signal, + ); + + const task = this.asyncTaskInfoService.create(async () => { + const { taskInfo } = await this.graphQLService.sdk.getAsyncTaskInfo({ taskId: result.id, removeOnFinish: false }); + return taskInfo; + }); + + cancelImplementation = () => this.asyncTaskInfoService.cancel(task.id); + controller.setMessage('plugin_data_import_process_file_processing_step_message'); + await this.asyncTaskInfoService.run(task); + + controller.resolve('plugin_data_import_process_success'); + return true; + } catch (exception: any) { + controller.reject(exception, 'plugin_data_import_process_fail'); + return false; + } finally { + cancelImplementation = null; + } + } +} diff --git a/webapp/packages/plugin-data-import/src/DataImportSettingsService.ts b/webapp/packages/plugin-data-import/src/DataImportSettingsService.ts new file mode 100644 index 0000000000..adfaf37eeb --- /dev/null +++ b/webapp/packages/plugin-data-import/src/DataImportSettingsService.ts @@ -0,0 +1,54 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { injectable } from '@cloudbeaver/core-di'; +import { HIGHEST_SETTINGS_LAYER } from '@cloudbeaver/core-root'; +import { + createSettingsOverrideResolver, + SettingsManagerService, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; + +const defaultSettings = schema.object({ + 'plugin.data-import.disabled': schemaExtra.stringedBoolean().default(false), +}); + +export type DataImportSettings = schema.infer; +export type DataImportSettingsSchema = typeof defaultSettings; + +@injectable(() => [SettingsProviderService, SettingsManagerService, SettingsResolverService]) +export class DataImportSettingsService { + get disabled(): boolean { + return this.settings.getValue('plugin.data-import.disabled'); + } + + readonly settings: SettingsProvider; + + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + private readonly settingsResolverService: SettingsResolverService, + ) { + // Some settings registered in plugin-data-editor-public-settings & permissions + this.settings = this.settingsProviderService.createSettings(defaultSettings); + + this.settingsResolverService.addResolver( + HIGHEST_SETTINGS_LAYER, + createSettingsOverrideResolver(this.settingsProviderService.settingsResolver, { + 'plugin.data-import.disabled': { + key: 'permission.data-editor.import', + map: value => !value, + }, + }), + ); + + this.settingsManagerService.registerSettings(() => []); + } +} diff --git a/webapp/packages/plugin-data-import/src/LocaleService.ts b/webapp/packages/plugin-data-import/src/LocaleService.ts new file mode 100644 index 0000000000..6d1a499028 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/LocaleService.ts @@ -0,0 +1,37 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable(() => [LocalizationService]) +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + override register(): void { + this.localizationService.addProvider(this.provider.bind(this)); + } + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru.js')).default; + case 'it': + return (await import('./locales/it.js')).default; + case 'zh': + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; + default: + return (await import('./locales/en.js')).default; + } + } +} diff --git a/webapp/packages/plugin-data-import/src/index.ts b/webapp/packages/plugin-data-import/src/index.ts new file mode 100644 index 0000000000..849f29f6ba --- /dev/null +++ b/webapp/packages/plugin-data-import/src/index.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +export { dataImportPluginManifest } from './manifest.js'; + +export * from './DataImportSettingsService.js'; diff --git a/webapp/packages/plugin-data-import/src/locales/en.ts b/webapp/packages/plugin-data-import/src/locales/en.ts new file mode 100644 index 0000000000..3c505d522f --- /dev/null +++ b/webapp/packages/plugin-data-import/src/locales/en.ts @@ -0,0 +1,7 @@ +export default [ + ['plugin_data_import_title', 'Data import'], + ['plugin_data_import_process_title', 'Importing data...'], + ['plugin_data_import_process_success', 'Data imported successfully'], + ['plugin_data_import_process_fail', 'Data import failed'], + ['plugin_data_import_process_file_processing_step_message', 'File uploaded, processing...'], +]; diff --git a/webapp/packages/plugin-data-import/src/locales/fr.ts b/webapp/packages/plugin-data-import/src/locales/fr.ts new file mode 100644 index 0000000000..ea7dd3ef6c --- /dev/null +++ b/webapp/packages/plugin-data-import/src/locales/fr.ts @@ -0,0 +1,7 @@ +export default [ + ['plugin_data_import_title', 'Importation de données'], + ['plugin_data_import_process_title', 'Importation des données...'], + ['plugin_data_import_process_success', 'Données importées avec succès'], + ['plugin_data_import_process_fail', "Échec de l'importation des données"], + ['plugin_data_import_process_file_processing_step_message', 'Fichier téléchargé, en cours de traitement...'], +]; diff --git a/webapp/packages/plugin-data-import/src/locales/it.ts b/webapp/packages/plugin-data-import/src/locales/it.ts new file mode 100644 index 0000000000..3c505d522f --- /dev/null +++ b/webapp/packages/plugin-data-import/src/locales/it.ts @@ -0,0 +1,7 @@ +export default [ + ['plugin_data_import_title', 'Data import'], + ['plugin_data_import_process_title', 'Importing data...'], + ['plugin_data_import_process_success', 'Data imported successfully'], + ['plugin_data_import_process_fail', 'Data import failed'], + ['plugin_data_import_process_file_processing_step_message', 'File uploaded, processing...'], +]; diff --git a/webapp/packages/plugin-data-import/src/locales/ru.ts b/webapp/packages/plugin-data-import/src/locales/ru.ts new file mode 100644 index 0000000000..7e08b40f55 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/locales/ru.ts @@ -0,0 +1,7 @@ +export default [ + ['plugin_data_import_title', 'Импорт данных'], + ['plugin_data_import_process_title', 'Импорт данных...'], + ['plugin_data_import_process_success', 'Данные успешно импортированы'], + ['plugin_data_import_process_fail', 'Ошибка импорта данных'], + ['plugin_data_import_process_file_processing_step_message', 'Файл загружен, обработка...'], +]; diff --git a/webapp/packages/plugin-data-import/src/locales/vi.ts b/webapp/packages/plugin-data-import/src/locales/vi.ts new file mode 100644 index 0000000000..940a3e7a08 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/locales/vi.ts @@ -0,0 +1,7 @@ +export default [ + ['plugin_data_import_title', 'Nhập dữ liệu'], + ['plugin_data_import_process_title', 'Đang nhập dữ liệu...'], + ['plugin_data_import_process_success', 'Dữ liệu đã được nhập thành công'], + ['plugin_data_import_process_fail', 'Nhập dữ liệu thất bại'], + ['plugin_data_import_process_file_processing_step_message', 'Tệp đã được tải lên, đang xử lý...'], +]; diff --git a/webapp/packages/plugin-data-import/src/locales/zh.ts b/webapp/packages/plugin-data-import/src/locales/zh.ts new file mode 100644 index 0000000000..f39b85c3b9 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/locales/zh.ts @@ -0,0 +1,7 @@ +export default [ + ['plugin_data_import_title', '数据导入'], + ['plugin_data_import_process_title', '正在导入数据...'], + ['plugin_data_import_process_success', '数据导入成功'], + ['plugin_data_import_process_fail', '数据导入失败'], + ['plugin_data_import_process_file_processing_step_message', '文件已上传,正在处理...'], +]; diff --git a/webapp/packages/plugin-data-import/src/manifest.ts b/webapp/packages/plugin-data-import/src/manifest.ts new file mode 100644 index 0000000000..d6655d2e91 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/manifest.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; + +export const dataImportPluginManifest: PluginManifest = { + info: { + name: 'Data Import Plugin', + }, +}; diff --git a/webapp/packages/plugin-data-import/src/module.ts b/webapp/packages/plugin-data-import/src/module.ts new file mode 100644 index 0000000000..d3517942a9 --- /dev/null +++ b/webapp/packages/plugin-data-import/src/module.ts @@ -0,0 +1,30 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Dependency, ModuleRegistry, proxy, Bootstrap } from '@cloudbeaver/core-di'; +import { LocaleService } from './LocaleService.js'; +import { DataImportService } from './DataImportService.js'; +import { DataImportProcessorsResource } from './DataImportProcessorsResource.js'; +import { DataImportSettingsService } from './DataImportSettingsService.js'; +import { DataImportBootstrap } from './DataImportBootstrap.js'; + +// force registration after export plugin +import '@cloudbeaver/plugin-data-export'; +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-data-import', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Dependency, proxy(DataImportProcessorsResource)) + .addSingleton(Bootstrap, DataImportBootstrap) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(DataImportService) + .addSingleton(DataImportProcessorsResource) + .addSingleton(DataImportSettingsService); + }, +}); diff --git a/webapp/packages/plugin-data-import/tsconfig.json b/webapp/packages/plugin-data-import/tsconfig.json new file mode 100644 index 0000000000..e2247b3f55 --- /dev/null +++ b/webapp/packages/plugin-data-import/tsconfig.json @@ -0,0 +1,67 @@ +{ + "extends": "@cloudbeaver/tsconfig/tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true + }, + "references": [ + { + "path": "../../common-typescript/@dbeaver/cli" + }, + { + "path": "../core-blocks" + }, + { + "path": "../core-cli" + }, + { + "path": "../core-di" + }, + { + "path": "../core-dialogs" + }, + { + "path": "../core-events" + }, + { + "path": "../core-localization" + }, + { + "path": "../core-resource" + }, + { + "path": "../core-root" + }, + { + "path": "../core-sdk" + }, + { + "path": "../core-settings" + }, + { + "path": "../core-utils" + }, + { + "path": "../core-view" + }, + { + "path": "../plugin-data-export" + }, + { + "path": "../plugin-data-viewer" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*" + ] +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/package.json b/webapp/packages/plugin-data-spreadsheet-new/package.json index 81c0bc4084..13330f4c11 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/package.json +++ b/webapp/packages/plugin-data-spreadsheet-new/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-data-spreadsheet-new", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,42 +11,52 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "pretest": "tsc -b", - "test": "core-cli-test", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "test": "dbeaver-test", + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@popperjs/core": "~2.11.8", - "react-popper": "~2.3.0", - "@cloudbeaver/plugin-data-viewer": "~0.1.0", - "@cloudbeaver/plugin-react-data-grid": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-data-context": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x", - "@testing-library/jest-dom": "~6.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-browser": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-settings": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-data-grid": "workspace:*", + "@cloudbeaver/plugin-data-viewer": "workspace:*", + "@dbeaver/js-helpers": "workspace:^", + "@dbeaver/result-set-api": "workspace:^", + "@dbeaver/ui-kit": "workspace:^", + "@popperjs/core": "^2", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "react-popper": "^2", + "tslib": "^2" }, "devDependencies": { - "@cloudbeaver/tests-runner": "~0.1.0" + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@dbeaver/cli": "workspace:*", + "@dbeaver/react-tests": "workspace:^", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5", + "vitest": "^3" } } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_ADD_ROW.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_ADD_ROW.ts new file mode 100644 index 0000000000..53ac1ada5d --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_ADD_ROW.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_EDITING_ADD_ROW = createAction('data-grid-editing-add-row', { + label: 'data_grid_table_editing_row_add', + icon: '/icons/data_add_sm.svg', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_DELETE_ROW.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_DELETE_ROW.ts new file mode 100644 index 0000000000..3512a01806 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_DELETE_ROW.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_EDITING_DELETE_ROW = createAction('data-grid-editing-delete-row', { + label: 'data_grid_table_editing_row_delete', + icon: '/icons/data_delete_sm.svg', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_DELETE_SELECTED_ROW.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_DELETE_SELECTED_ROW.ts new file mode 100644 index 0000000000..195b6933c4 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_DELETE_SELECTED_ROW.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_EDITING_DELETE_SELECTED_ROW = createAction('data-grid-editing-delete-selected-row', { + label: 'data_viewer_action_edit_delete', + icon: '/icons/data_delete_sm.svg', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_DUPLICATE_ROW.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_DUPLICATE_ROW.ts new file mode 100644 index 0000000000..830b0af449 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_DUPLICATE_ROW.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_EDITING_DUPLICATE_ROW = createAction('data-grid-editing-duplicate-row', { + label: 'data_grid_table_editing_row_add_copy', + icon: '/icons/data_add_copy_sm.svg', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_REVERT_ROW.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_REVERT_ROW.ts new file mode 100644 index 0000000000..19b77e0e11 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_REVERT_ROW.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_EDITING_REVERT_ROW = createAction('data-grid-editing-revert-row', { + label: 'data_grid_table_editing_row_revert', + icon: '/icons/data_revert_sm.svg', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW.ts new file mode 100644 index 0000000000..482a74d59b --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW = createAction('data-grid-editing-revert-selected-row', { + label: 'data_viewer_action_edit_revert', + icon: '/icons/data_revert_sm.svg', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_SET_TO_NULL.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_SET_TO_NULL.ts new file mode 100644 index 0000000000..06f0be10de --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Editing/ACTION_DATA_GRID_EDITING_SET_TO_NULL.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_EDITING_SET_TO_NULL = createAction('data-grid-editing-set-to-null', { + label: 'data_grid_table_editing_set_to_null', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Filters/ACTION_DATA_GRID_FILTERS_RESET_ALL.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Filters/ACTION_DATA_GRID_FILTERS_RESET_ALL.ts new file mode 100644 index 0000000000..e902d73bc3 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Filters/ACTION_DATA_GRID_FILTERS_RESET_ALL.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_FILTERS_RESET_ALL = createAction('filters-reset-all', { + label: 'data_grid_table_filter_reset_all_filters', + icon: 'filter-reset-all', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Ordering/ACTION_DATA_GRID_ORDERING_DISABLE_ALL.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Ordering/ACTION_DATA_GRID_ORDERING_DISABLE_ALL.ts new file mode 100644 index 0000000000..b37c07cb56 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Ordering/ACTION_DATA_GRID_ORDERING_DISABLE_ALL.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_ORDERING_DISABLE_ALL = createAction('data-grid-ordering-disable-all', { + label: 'data_grid_table_disable_all_orders', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.m.css deleted file mode 100644 index 8dd6684737..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.m.css +++ /dev/null @@ -1,23 +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. - */ - -.editor { - composes: theme-typography--body2 from global; -} -.box { - position: absolute; - left: 0; - top: 0; - width: 0; - height: 100%; -} -.inlineEditor { - font-size: 12px; - left: -1px; - top: 0; -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.tsx deleted file mode 100644 index ffcc6095ec..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.tsx +++ /dev/null @@ -1,138 +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 { observer } from 'mobx-react-lite'; -import { forwardRef, useContext, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { usePopper } from 'react-popper'; - -import { s, useS } from '@cloudbeaver/core-blocks'; -import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; -import { InlineEditor } from '@cloudbeaver/core-ui'; -import type { IResultSetElementKey, IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer'; -import type { RenderEditCellProps } from '@cloudbeaver/plugin-react-data-grid'; - -import style from './CellEditor.m.css'; -import { DataGridContext, IColumnResizeInfo } from './DataGridContext'; -import { TableDataContext } from './TableDataContext'; - -export interface IEditorRef { - focus: () => void; -} - -const lockNavigation = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter']; - -export const CellEditor = observer, 'row' | 'column' | 'onClose'>, IEditorRef>( - forwardRef(function CellEditor({ row, column, onClose }, ref) { - const dataGridContext = useContext(DataGridContext); - const tableDataContext = useContext(TableDataContext); - const inputRef = useRef(null); - const [elementRef, setElementRef] = useState(null); - const [popperRef, setPopperRef] = useState(null); - const popper = usePopper(elementRef, popperRef, { - placement: 'right', - modifiers: [{ name: 'flip', enabled: false }], - }); - const styles = useS(style); - - if (!dataGridContext || !tableDataContext || column.columnDataIndex === null) { - throw new Error('DataGridContext should be provided'); - } - - useImperativeHandle(ref, () => ({ - focus: () => inputRef.current?.focus(), - })); - - useEffect(() => { - function resize(data: IColumnResizeInfo) { - if (elementRef && popperRef && data.column === column.idx) { - popperRef.style.width = data.width + 1 + 'px'; - } - } - - dataGridContext.columnResize.addHandler(resize); - - return () => dataGridContext.columnResize.removeHandler(resize); - }, [elementRef, popperRef, column]); - - useLayoutEffect(() => { - if (elementRef && popperRef) { - const size = elementRef.closest('[role="gridcell"]')?.getBoundingClientRect(); - - if (size) { - popperRef.style.width = size.width + 1 + 'px'; - popperRef.style.height = size.height + 1 + 'px'; - } - } - }); - - const cellKey: IResultSetElementKey = { row, column: column.columnDataIndex }; - - const value = tableDataContext.format.getText(tableDataContext.getCellValue(cellKey)!) ?? ''; - - const handleSave = () => onClose(false); - const handleReject = () => { - tableDataContext.editor.revert(cellKey); - onClose(false); - }; - const handleChange = (value: string) => { - tableDataContext.editor.set(cellKey, value); - }; - const handleUndo = () => { - tableDataContext.editor.revert(cellKey); - onClose(false); - }; - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (lockNavigation.includes(event.key)) { - event.stopPropagation(); - } - }; - - const preventClick = (event: React.MouseEvent) => { - EventContext.set(event, EventStopPropagationFlag); // better but not works - event.stopPropagation(); - }; - - return ( -
- { - createPortal( -
- -
, - dataGridContext.getEditorPortal()!, - ) as any - } -
- ); - }), -); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellContext.ts index a9b221ea2c..e0d343302c 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellContext.ts @@ -1,25 +1,28 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createContext } from 'react'; -import type { IMouseHook } from '@cloudbeaver/core-blocks'; import type { DatabaseEditChangeType, IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer'; -import type { CellPosition } from '../../Editing/EditingContext'; +import type { IColumnInfo } from '../TableDataContext.js'; +import type { ICellPosition } from '@cloudbeaver/plugin-data-grid'; export interface ICellContext { - mouse: IMouseHook; - cell: IResultSetElementKey | undefined; - position: CellPosition; - isEditing: boolean; - isSelected: boolean; + isHovered: boolean; isFocused: boolean; + isSelected: boolean; + column: IColumnInfo; + cell: IResultSetElementKey | undefined; + position: ICellPosition; editionState: DatabaseEditChangeType | null; + isMenuVisible: boolean; + setMenuVisibility(visible: boolean): void; + setHover(hovered: boolean): void; } export const CellContext = createContext(undefined as any); 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..7bddf81291 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 @@ -1,107 +1,119 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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, observable } from 'mobx'; +import { computed, observable, action } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useContext, useEffect } from 'react'; +import { useContext, type HTMLAttributes } from 'react'; -import { getComputed, useCombinedHandler, useMouse, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks'; +import { getComputed, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; -import { clsx } from '@cloudbeaver/core-utils'; -import { DatabaseEditChangeType, IResultSetElementKey, IResultSetRowKey, isBooleanValuePresentationAvailable } from '@cloudbeaver/plugin-data-viewer'; -import { CalculatedColumn, Cell, CellRendererProps } from '@cloudbeaver/plugin-react-data-grid'; - -import { CellPosition, EditingContext } from '../../Editing/EditingContext'; -import { DataGridContext } from '../DataGridContext'; -import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext'; -import { TableDataContext } from '../TableDataContext'; -import { CellContext } from './CellContext'; - -export const CellRenderer = observer>(function CellRenderer(props) { - const { row, column, isCellSelected, onDoubleClick, selectCell } = props; +import { clsx } from '@dbeaver/ui-kit'; +import { type IDataGridCellRenderer, type ICellPosition } from '@cloudbeaver/plugin-data-grid'; +import { DatabaseEditChangeType, type IResultSetElementKey, type IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer'; + +import { DataGridContext } from '../DataGridContext.js'; +import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext.js'; +import { TableDataContext, type IColumnInfo } from '../TableDataContext.js'; +import { CellContext } from './CellContext.js'; + +interface Props { + rowIdx: number; + colIdx: number; + props: HTMLAttributes; + renderDefaultCell: IDataGridCellRenderer; +} + +export const CellRenderer = observer(function CellRenderer({ rowIdx, colIdx, props, renderDefaultCell }) { const dataGridContext = useContext(DataGridContext); const tableDataContext = useContext(TableDataContext); const selectionContext = useContext(DataGridSelectionContext); - const editingContext = useContext(EditingContext); - const mouse = useMouse({}); - - const rowIdx = tableDataContext.getRowIndexFromKey(row); const cellContext = useObservableRef( () => ({ - mouse, - get position(): CellPosition { - return { idx: this.column.idx, rowIdx: this.rowIdx }; + isHovered: false, + isMenuVisible: false, + get position(): ICellPosition { + return { colIdx: this.colIdx, rowIdx: this.rowIdx }; + }, + get column(): IColumnInfo { + return this.tableDataContext.getColumn(this.colIdx)!; + }, + get row(): IResultSetRowKey | undefined { + return this.tableDataContext.getRow(this.rowIdx); }, get cell(): IResultSetElementKey | undefined { - if (this.column.columnDataIndex === null) { + if (this.column.key === null || this.row === undefined) { return undefined; } - return { row: this.row, column: this.column.columnDataIndex }; + return { row: this.row, column: this.column.key }; }, - get isEditing(): boolean { - return editingContext.isEditing(this.position) || false; + get isFocused(): boolean { + return this.props['aria-selected'] === 'true'; }, get isSelected(): boolean { - return selectionContext.isSelected(this.position.rowIdx, this.position.idx) || false; + return this.selectionContext.isSelected(this.position.rowIdx, this.position.colIdx) || false; }, - get isFocused(): boolean { - return this.isEditing ? false : this.isCellSelected; + get hasFocusedElementInRow(): boolean { + const focusedElement = this.focusedElementPosition; + return focusedElement?.rowIdx === this.position.rowIdx; + }, + get focusedElementPosition() { + return this.selectionContext.getFocusedElementPosition(); }, get editionState(): DatabaseEditChangeType | null { if (!this.cell) { return null; } - return tableDataContext.getEditionState(this.cell); + return this.tableDataContext.getEditionState(this.cell); + }, + setMenuVisibility(visibility: boolean): void { + this.isMenuVisible = visibility; + }, + setHover(hovered: boolean) { + this.isHovered = hovered; }, }), { - row: observable.ref, - column: observable.ref, + isHovered: observable.ref, + isMenuVisible: observable.ref, + setMenuVisibility: action, + colIdx: observable.ref, rowIdx: observable.ref, - isCellSelected: observable.ref, + row: computed, + column: computed, position: computed, cell: computed, - isEditing: computed, + hasFocusedElementInRow: computed, + focusedElementPosition: computed, isSelected: computed, - isFocused: computed, editionState: computed, + tableDataContext: observable.ref, + selectionContext: observable.ref, + props: observable.ref, + setHover: action.bound, }, - { row, column, rowIdx, isCellSelected }, + { colIdx, rowIdx, tableDataContext, selectionContext, props }, + ); + + const isDatabaseActionApplied = getComputed(() => + [DatabaseEditChangeType.add, DatabaseEditChangeType.delete, DatabaseEditChangeType.update].includes(cellContext.editionState!), ); const classes = getComputed(() => clsx({ + 'rdg-cell-custom-highlighted-row': cellContext.hasFocusedElementInRow && !isDatabaseActionApplied, 'rdg-cell-custom-selected': cellContext.isSelected, - 'rdg-cell-custom-editing': cellContext.isEditing, 'rdg-cell-custom-added': cellContext.editionState === DatabaseEditChangeType.add, 'rdg-cell-custom-deleted': cellContext.editionState === DatabaseEditChangeType.delete, 'rdg-cell-custom-edited': cellContext.editionState === DatabaseEditChangeType.update, }), ); - function isEditable(column: CalculatedColumn): boolean { - if (!cellContext.cell) { - return false; - } - - const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column); - const value = tableDataContext.getCellValue(cellContext.cell); - - if (!resultColumn || value === undefined) { - return false; - } - - const handleByBooleanFormatter = isBooleanValuePresentationAvailable(value, resultColumn); - - return !(handleByBooleanFormatter || tableDataContext.isCellReadonly(cellContext.cell)); - } - const state = useObjectRef( () => ({ mouseDown(event: React.MouseEvent) { @@ -117,7 +129,7 @@ export const CellRenderer = observer) { - if ( - !this.isEditable(this.column) || - // !this.dataGridContext.isGridInFocus() - EventContext.has(event, EventStopPropagationFlag) - ) { - return; - } - - this.editingContext.edit(cellContext.position); - }, }), { - row, - column, + colIdx, rowIdx, selectionContext, dataGridContext, - editingContext, - isEditable, - selectCell, }, - ['doubleClick', 'mouseUp', 'mouseDown'], + ['mouseUp', 'mouseDown'], ); - useEffect(() => () => editingContext.closeEditor(cellContext.position), []); - const handleDoubleClick = useCombinedHandler(state.doubleClick, onDoubleClick); - return ( - + {renderDefaultCell({ + className: classes, + 'data-row-index': rowIdx, + 'data-column-index': colIdx, + onMouseDown: state.mouseDown, + onMouseUp: state.mouseUp, + onPointerEnter: () => cellContext.setHover(true), + onPointerLeave: () => cellContext.setHover(false), + })} ); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DATA_GRID_BINDINGS.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DATA_GRID_BINDINGS.ts new file mode 100644 index 0000000000..a30842c03c --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DATA_GRID_BINDINGS.ts @@ -0,0 +1,29 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IKeyBinding } from '@cloudbeaver/core-view'; + +/* these consts are only used for the user interface in Shortcuts popup, actual bindings in DataGridTable.tsx */ +export const KEY_BINDING_REVERT_INLINE_EDITOR_CHANGES: IKeyBinding = { + id: 'data-viewer-revert-inline-editor-changes', + keys: ['Escape'], +}; + +export const KEY_BINDING_ADD_NEW_ROW: IKeyBinding = { + id: 'data-viewer-add-new-row', + keys: ['Alt+R'], +}; + +export const KEY_BINDING_DUPLICATE_ROW: IKeyBinding = { + id: 'data-viewer-duplicate-row', + keys: ['Shift+Alt+R'], +}; + +export const KEY_BINDING_DELETE_ROW: IKeyBinding = { + id: 'data-viewer-delete-row', + keys: ['Delete'], +}; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts index 34bd583d33..d7430f3c2b 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts @@ -1,15 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { createContext } from 'react'; -import type { IExecutor } from '@cloudbeaver/core-executor'; +import type { DataGridRef } from '@cloudbeaver/plugin-data-grid'; import type { IDatabaseDataModel, IDataTableActions } from '@cloudbeaver/plugin-data-viewer'; -import type { DataGridHandle } from '@cloudbeaver/plugin-react-data-grid'; export interface IColumnResizeInfo { column: number; @@ -21,10 +20,8 @@ export interface IDataGridContext { actions: IDataTableActions; resultIndex: number; simple: boolean; - columnResize: IExecutor; isGridInFocus: () => boolean; - getEditorPortal: () => HTMLDivElement | null; - getDataGridApi: () => DataGridHandle | null; + getDataGridApi: () => DataGridRef | null; focus: () => void; } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts index f8278fae6f..5d9995faf0 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts @@ -1,215 +1,206 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { injectable } from '@cloudbeaver/core-di'; +import { ACTION_EDIT, ActionService, getBindingLabel, KEY_BINDING_ADD, KEY_BINDING_DUPLICATE, MenuService, type IAction } from '@cloudbeaver/core-view'; import { + DATA_CONTEXT_DV_DDM, + DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_PRESENTATION_ACTIONS, + DATA_CONTEXT_DV_RESULT_KEY, DatabaseEditChangeType, isBooleanValuePresentationAvailable, + isResultSetDataSource, + ResultSetDataContentAction, + ResultSetDataSource, ResultSetEditAction, ResultSetFormatAction, ResultSetSelectAction, ResultSetViewAction, } from '@cloudbeaver/plugin-data-viewer'; - -import { DataGridContextMenuService } from './DataGridContextMenuService'; - -@injectable() +import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +import { ACTION_DATA_GRID_EDITING_ADD_ROW } from '../Actions/Editing/ACTION_DATA_GRID_EDITING_ADD_ROW.js'; +import { ACTION_DATA_GRID_EDITING_DELETE_ROW } from '../Actions/Editing/ACTION_DATA_GRID_EDITING_DELETE_ROW.js'; +import { ACTION_DATA_GRID_EDITING_DELETE_SELECTED_ROW } from '../Actions/Editing/ACTION_DATA_GRID_EDITING_DELETE_SELECTED_ROW.js'; +import { ACTION_DATA_GRID_EDITING_DUPLICATE_ROW } from '../Actions/Editing/ACTION_DATA_GRID_EDITING_DUPLICATE_ROW.js'; +import { ACTION_DATA_GRID_EDITING_REVERT_ROW } from '../Actions/Editing/ACTION_DATA_GRID_EDITING_REVERT_ROW.js'; +import { ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW } from '../Actions/Editing/ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW.js'; +import { ACTION_DATA_GRID_EDITING_SET_TO_NULL } from '../Actions/Editing/ACTION_DATA_GRID_EDITING_SET_TO_NULL.js'; +import { MENU_DATA_GRID_EDITING } from './MENU_DATA_GRID_EDITING.js'; + +@injectable(() => [ActionService, LocalizationService, MenuService]) export class DataGridContextMenuCellEditingService { - private static readonly menuEditingToken = 'menuEditing'; + constructor( + private readonly actionService: ActionService, + private readonly localizationService: LocalizationService, + private readonly menuService: MenuService, + ) { } - constructor(private readonly dataGridContextMenuService: DataGridContextMenuService) {} - getMenuEditingToken(): string { - return DataGridContextMenuCellEditingService.menuEditingToken; - } register(): void { - this.dataGridContextMenuService.add(this.dataGridContextMenuService.getMenuToken(), { - id: this.getMenuEditingToken(), - order: 4, - title: 'data_grid_table_editing', - icon: 'edit', - isPanel: true, - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - return context.data.model.isDisabled(context.data.resultIndex) || context.data.model.isReadonly(context.data.resultIndex); - }, + this.menuService.addCreator({ + root: true, + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, DATA_CONTEXT_DV_RESULT_KEY], + isApplicable: context => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + return isResultSetDataSource(model.source) && !model.isDisabled(resultIndex) && !model.isReadonly(resultIndex); + }, + getItems: (context, items) => [...items, MENU_DATA_GRID_EDITING], }); - this.dataGridContextMenuService.add(this.getMenuEditingToken(), { - id: 'open_inline_editor', - order: 0, - title: 'data_grid_table_editing_open_inline_editor', - icon: 'edit', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - const format = context.data.model.source.getAction(context.data.resultIndex, ResultSetFormatAction); - const view = context.data.model.source.getAction(context.data.resultIndex, ResultSetViewAction); - const cellValue = view.getCellValue(context.data.key); - const column = view.getColumn(context.data.key.column); - - if (!column || cellValue === undefined || format.isReadOnly(context.data.key)) { - return true; - } - return isBooleanValuePresentationAvailable(cellValue, column); - }, - onClick(context) { - context.data.spreadsheetActions.edit(context.data.key); - }, + this.menuService.addCreator({ + menus: [MENU_DATA_GRID_EDITING], + getItems: (context, items) => [ + ...items, + ACTION_EDIT, + ACTION_DATA_GRID_EDITING_SET_TO_NULL, + ACTION_DATA_GRID_EDITING_ADD_ROW, + ACTION_DATA_GRID_EDITING_DUPLICATE_ROW, + ACTION_DATA_GRID_EDITING_DELETE_ROW, + ACTION_DATA_GRID_EDITING_DELETE_SELECTED_ROW, + ACTION_DATA_GRID_EDITING_REVERT_ROW, + ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW, + ], }); - this.dataGridContextMenuService.add(this.getMenuEditingToken(), { - id: 'set_to_null', - order: 1, - title: 'data_grid_table_editing_set_to_null', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - const { key, model, resultIndex } = context.data; - const view = model.source.getAction(resultIndex, ResultSetViewAction); - const format = model.source.getAction(resultIndex, ResultSetFormatAction); + + this.actionService.addHandler({ + id: 'data-grid-editing-base-handler', + menus: [MENU_DATA_GRID_EDITING], + isActionApplicable(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const format = source.getAction(resultIndex, ResultSetFormatAction); + const view = source.getAction(resultIndex, ResultSetViewAction); + const content = source.getAction(resultIndex, ResultSetDataContentAction); + const editor = source.getAction(resultIndex, ResultSetEditAction); + const select = source.getActionImplementation(resultIndex, ResultSetSelectAction); + const cellValue = view.getCellValue(key); + const column = view.getColumn(key.column); + const isComplex = format.isBinary(key) || format.isGeometry(key); + const isTruncated = content.isTextTruncated(key); + const selectedElements = select?.getSelectedElements() || []; + // If we somehow added a new row, we can always edit it + const canEdit = editor.getElementState(key) === DatabaseEditChangeType.add; - return cellValue === undefined || format.isReadOnly(context.data.key) || view.getColumn(key.column)?.required || format.isNull(cellValue); - }, - onClick(context) { - context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction).set(context.data.key, null); - }, - }); - this.dataGridContextMenuService.add(this.getMenuEditingToken(), { - id: 'row_add', - order: 5, - icon: '/icons/data_add_sm.svg', - title: 'data_grid_table_editing_row_add', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); - return !editor.hasFeature('add'); - }, - onClick(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); - editor.addRow(context.data.key.row); - }, - }); - this.dataGridContextMenuService.add(this.getMenuEditingToken(), { - id: 'row_add_copy', - order: 5.5, - icon: '/icons/data_add_copy_sm.svg', - title: 'data_grid_table_editing_row_add_copy', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); - return !editor.hasFeature('add'); - }, - onClick(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); - editor.duplicateRow(context.data.key.row); - }, - }); - this.dataGridContextMenuService.add(this.getMenuEditingToken(), { - id: 'row_delete', - order: 6, - icon: '/icons/data_delete_sm.svg', - title: 'data_grid_table_editing_row_delete', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); + if (model.isReadonly(resultIndex)) { + return false; + } + + if (action === ACTION_EDIT) { + if (!column || cellValue === undefined || (format.isReadOnly(key) && !canEdit) || isComplex || isTruncated) { + return false; + } - if (context.data.model.isReadonly(context.data.resultIndex) || !editor.hasFeature('delete')) { - return true; + return !isBooleanValuePresentationAvailable(cellValue, column); } - const format = context.data.model.source.getAction(context.data.resultIndex, ResultSetFormatAction); - return format.isReadOnly(context.data.key) || editor.getElementState(context.data.key) === DatabaseEditChangeType.delete; - }, - onClick(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); - editor.deleteRow(context.data.key.row); - }, - }); - this.dataGridContextMenuService.add(this.getMenuEditingToken(), { - id: 'row_delete_selected', - order: 6.1, - icon: '/icons/data_delete_sm.svg', - title: 'data_viewer_action_edit_delete', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); + if (action === ACTION_DATA_GRID_EDITING_SET_TO_NULL) { + return cellValue !== undefined && !(format.isReadOnly(key) && !canEdit) && !view.getColumn(key.column)?.required && !format.isNull(key); + } - if (context.data.model.isReadonly(context.data.resultIndex) || !editor.hasFeature('delete')) { - return true; + if (action === ACTION_DATA_GRID_EDITING_ADD_ROW || action === ACTION_DATA_GRID_EDITING_DUPLICATE_ROW) { + return editor.hasFeature('add'); } - const select = context.data.model.source.getActionImplementation(context.data.resultIndex, ResultSetSelectAction); + if (action === ACTION_DATA_GRID_EDITING_DELETE_ROW) { + return !(format.isReadOnly(key) && !canEdit) && editor.getElementState(key) !== DatabaseEditChangeType.delete; + } - const selectedElements = select?.getSelectedElements() || []; + if (action === ACTION_DATA_GRID_EDITING_DELETE_SELECTED_ROW) { + if ((format.isReadOnly(key) && !canEdit) || !editor.hasFeature('delete')) { + return false; + } - return !selectedElements.some(key => editor.getElementState(key) !== DatabaseEditChangeType.delete); - }, - onClick(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); - const select = context.data.model.source.getActionImplementation(context.data.resultIndex, ResultSetSelectAction); + return selectedElements.some(key => editor.getElementState(key) !== DatabaseEditChangeType.delete); + } - const selectedElements = select?.getSelectedElements() || []; + if (action === ACTION_DATA_GRID_EDITING_REVERT_ROW) { + return editor.getElementState(key) !== null; + } - editor.delete(...selectedElements); - }, - }); - this.dataGridContextMenuService.add(this.getMenuEditingToken(), { - id: 'row_revert', - order: 7, - icon: '/icons/data_revert_sm.svg', - title: 'data_grid_table_editing_row_revert', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); - return editor.getElementState(context.data.key) === null; - }, - onClick(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); - editor.revert(context.data.key); - }, - }); - this.dataGridContextMenuService.add(this.getMenuEditingToken(), { - id: 'row_revert_selected', - order: 7.1, - icon: '/icons/data_revert_sm.svg', - title: 'data_viewer_action_edit_revert', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); - const select = context.data.model.source.getActionImplementation(context.data.resultIndex, ResultSetSelectAction); + if (action === ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW) { + return selectedElements.some(key => editor.getElementState(key) !== null); + } - const selectedElements = select?.getSelectedElements() || []; - return !selectedElements.some(key => editor.getElementState(key) !== null); - }, - onClick(context) { - const editor = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); - const select = context.data.model.source.getActionImplementation(context.data.resultIndex, ResultSetSelectAction); + return [ + ACTION_EDIT, + ACTION_DATA_GRID_EDITING_SET_TO_NULL, + ACTION_DATA_GRID_EDITING_ADD_ROW, + ACTION_DATA_GRID_EDITING_DUPLICATE_ROW, + ACTION_DATA_GRID_EDITING_DELETE_ROW, + ACTION_DATA_GRID_EDITING_DELETE_SELECTED_ROW, + ACTION_DATA_GRID_EDITING_REVERT_ROW, + ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW, + ].includes(action); + }, + getActionInfo: this.getActionInfo.bind(this), + handler(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const actions = context.get(DATA_CONTEXT_DV_PRESENTATION_ACTIONS)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const editor = source.getAction(resultIndex, ResultSetEditAction); + const select = source.getActionImplementation(resultIndex, ResultSetSelectAction); const selectedElements = select?.getSelectedElements() || []; - editor.revert(...selectedElements); + + switch (action) { + case ACTION_EDIT: + actions.edit(key); + break; + case ACTION_DATA_GRID_EDITING_SET_TO_NULL: + editor.set(key, null); + break; + case ACTION_DATA_GRID_EDITING_ADD_ROW: + editor.addRow(key.row); + break; + case ACTION_DATA_GRID_EDITING_DUPLICATE_ROW: + editor.duplicateRow(key); + break; + case ACTION_DATA_GRID_EDITING_DELETE_ROW: + editor.deleteRow(key.row); + break; + case ACTION_DATA_GRID_EDITING_DELETE_SELECTED_ROW: + editor.delete(...selectedElements); + break; + case ACTION_DATA_GRID_EDITING_REVERT_ROW: + editor.revert(key); + break; + case ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW: + editor.revert(...selectedElements); + break; + } }, }); } + + private getActionInfo(context: IDataContextProvider, action: IAction) { + const t = this.localizationService.translate; + if (action === ACTION_DATA_GRID_EDITING_ADD_ROW) { + return { ...action.info, label: 'data_grid_table_editing_row_add', tooltip: t('data_grid_table_editing_row_add') + ' (' + getBindingLabel(KEY_BINDING_ADD) + ')' }; + } + if (action === ACTION_DATA_GRID_EDITING_DUPLICATE_ROW) { + return { ...action.info, label: 'data_grid_table_editing_row_add_copy', tooltip: t('data_grid_table_editing_row_add_copy') + ' (' + getBindingLabel(KEY_BINDING_DUPLICATE) + ')' }; + } + + if (action === ACTION_EDIT) { + return { ...action.info, label: t('data_grid_table_editing_open_inline_editor'), icon: 'edit' }; + } + + return action.info; + } } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts index c74e9c1d6d..20ff242019 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts @@ -1,50 +1,53 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { importLazyComponent } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, ComputedContextMenuModel, DialogueStateResult, IContextMenuItem, IMenuContext } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { ClipboardService } from '@cloudbeaver/core-ui'; import { replaceMiddle } from '@cloudbeaver/core-utils'; +import { ACTION_DELETE, ActionService, MenuBaseItem, MenuService } from '@cloudbeaver/core-view'; import { - IDatabaseDataModel, - IDatabaseDataOptions, - IDatabaseResultSet, - IResultSetColumnKey, + DATA_CONTEXT_DV_DDM, + DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_RESULT_KEY, + DatabaseDataConstraintAction, + type IDatabaseDataModel, + type IResultSetColumnKey, IS_NOT_NULL_ID, IS_NULL_ID, isFilterConstraint, + isResultSetDataSource, nullOperationsFilter, - ResultSetConstraintAction, ResultSetDataAction, + ResultSetDataSource, ResultSetFormatAction, wrapOperationArgument, } from '@cloudbeaver/plugin-data-viewer'; -import { DataGridContextMenuService, IDataGridCellMenuContext } from '../DataGridContextMenuService'; -import { FilterCustomValueDialog } from './FilterCustomValueDialog'; +import { ACTION_DATA_GRID_FILTERS_RESET_ALL } from '../../Actions/Filters/ACTION_DATA_GRID_FILTERS_RESET_ALL.js'; +import { MENU_DATA_GRID_FILTERS } from './MENU_DATA_GRID_FILTERS.js'; +import { MENU_DATA_GRID_FILTERS_CELL_VALUE } from './MENU_DATA_GRID_FILTERS_CELL_VALUE.js'; +import { MENU_DATA_GRID_FILTERS_CLIPBOARD } from './MENU_DATA_GRID_FILTERS_CLIPBOARD.js'; +import { MENU_DATA_GRID_FILTERS_CUSTOM } from './MENU_DATA_GRID_FILTERS_CUSTOM.js'; -@injectable() -export class DataGridContextMenuFilterService { - private static readonly menuFilterToken = 'menuFilter'; +const FilterCustomValueDialog = importLazyComponent(() => import('./FilterCustomValueDialog.js').then(m => m.FilterCustomValueDialog)); +@injectable(() => [CommonDialogService, ClipboardService, ActionService, MenuService]) +export class DataGridContextMenuFilterService { constructor( - private readonly dataGridContextMenuService: DataGridContextMenuService, private readonly commonDialogService: CommonDialogService, private readonly clipboardService: ClipboardService, - ) { - this.dataGridContextMenuService.onRootMenuOpen.addHandler(this.getClipboardValue.bind(this)); - } - - getMenuFilterToken(): string { - return DataGridContextMenuFilterService.menuFilterToken; - } + private readonly actionService: ActionService, + private readonly menuService: MenuService, + ) {} private async applyFilter( - model: IDatabaseDataModel, + model: IDatabaseDataModel, resultIndex: number, column: IResultSetColumnKey, operator: string, @@ -54,7 +57,7 @@ export class DataGridContextMenuFilterService { return; } - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const constraints = model.source.getAction(resultIndex, DatabaseDataConstraintAction); const data = model.source.getAction(resultIndex, ResultSetDataAction); const resultColumn = data.getColumn(column); @@ -62,220 +65,268 @@ export class DataGridContextMenuFilterService { throw new Error(`Failed to get result column info for the following column index: "${column.index}"`); } - await model.requestDataAction(async () => { + await model.request(() => { constraints.setFilter(resultColumn.position, operator, filterValue); - await model.request(true); }); } - private async getClipboardValue() { - if (this.clipboardService.state === 'granted') { - await this.clipboardService.read(); - } - } + register(): void { + this.menuService.addCreator({ + root: true, + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, DATA_CONTEXT_DV_RESULT_KEY], + isApplicable: context => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; - private getGeneralizedMenuItems( - context: IMenuContext, - value: any | (() => any), - icon: string, - isHidden?: (context: IMenuContext) => boolean, - ): Array> { - 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 || ''; - - return supportedOperations - .filter(operation => !nullOperationsFilter(operation)) - .map(operation => ({ - id: operation.id, - icon, - isPresent: () => true, - isDisabled(context) { - return context.data.model.isLoading(); - }, - isHidden(context) { - return isHidden?.(context) ?? false; - }, - titleGetter() { - const val = typeof value === 'function' ? value() : value; - const stringifyValue = format.toDisplayString(val); - const wrappedValue = wrapOperationArgument(operation.id, stringifyValue); - const clippedValue = replaceMiddle(wrappedValue, ' ... ', 8, 30); - return `${columnLabel} ${operation.expression} ${clippedValue}`; - }, - onClick: async () => { - const val = typeof value === 'function' ? value() : value; - const wrappedValue = wrapOperationArgument(operation.id, val); - await this.applyFilter(model, resultIndex, key.column, operation.id, wrappedValue); - }, - })); - } + const source = model.source as unknown as ResultSetDataSource; - register(): void { - this.dataGridContextMenuService.add(this.dataGridContextMenuService.getMenuToken(), { - id: this.getMenuFilterToken(), - order: 2, - title: 'data_grid_table_filter', - icon: 'filter', - isPanel: true, - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - if (context.data.model.isDisabled(context.data.resultIndex)) { - return true; + if (!isResultSetDataSource(source)) { + return false; } - const constraints = context.data.model.source.getAction(context.data.resultIndex, ResultSetConstraintAction); - return !constraints.supported; + const constraints = source.getAction(resultIndex, DatabaseDataConstraintAction); + return constraints.supported && !model.isDisabled(resultIndex); }, + getItems: (context, items) => [...items, MENU_DATA_GRID_FILTERS], }); - this.dataGridContextMenuService.add(this.dataGridContextMenuService.getMenuToken(), { - id: 'deleteFiltersAndOrders', - order: 3, - title: 'data_grid_table_delete_filters_and_orders', - icon: 'erase', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - if (context.data.model.isDisabled(context.data.resultIndex)) { - return true; + + this.menuService.addCreator({ + menus: [MENU_DATA_GRID_FILTERS], + getItems: (context, items) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const data = source.getAction(resultIndex, ResultSetDataAction); + const resultColumn = data.getColumn(key.column); + + const supportedOperations = data.getColumnOperations(key.column); + const result = []; + + for (const filter of [IS_NULL_ID, IS_NOT_NULL_ID]) { + const label = `${resultColumn ? `"${resultColumn.label}" ` : ''}${filter.split('_').join(' ')}`; + + if (supportedOperations.some(operation => operation.id === filter)) { + result.push( + new MenuBaseItem( + { + id: filter, + label, + icon: 'filter', + }, + { + onSelect: async () => { + await this.applyFilter(model as unknown as IDatabaseDataModel, resultIndex, key.column, filter); + }, + }, + ), + ); + } } - const constraints = context.data.model.source.getAction(context.data.resultIndex, ResultSetConstraintAction); - return constraints.orderConstraints.length === 0 && constraints.filterConstraints.length === 0; - }, - onClick: async context => { - const { model, resultIndex } = context.data; - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - - await model.requestDataAction(async () => { - constraints.deleteData(); - await model.request(true); - }); + return [ + ...items, + MENU_DATA_GRID_FILTERS_CELL_VALUE, + MENU_DATA_GRID_FILTERS_CUSTOM, + MENU_DATA_GRID_FILTERS_CLIPBOARD, + ...result, + ACTION_DELETE, + ACTION_DATA_GRID_FILTERS_RESET_ALL, + ]; }, }); - this.dataGridContextMenuService.add(this.getMenuFilterToken(), { - id: 'clipboardValue', - order: 0, - title: 'ui_clipboard', - icon: 'filter-clipboard', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; + + this.actionService.addHandler({ + id: 'data-grid-filters-base-handler', + menus: [MENU_DATA_GRID_FILTERS], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, DATA_CONTEXT_DV_RESULT_KEY], + isActionApplicable: (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + + if (!isResultSetDataSource(model.source)) { + return false; + } + + return [ACTION_DELETE, ACTION_DATA_GRID_FILTERS_RESET_ALL].includes(action); }, - isHidden: context => { - if (!this.clipboardService.clipboardAvailable || this.clipboardService.state === 'denied') { - return true; + isHidden: (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const data = source.getAction(resultIndex, ResultSetDataAction); + + if (action === ACTION_DELETE) { + const constraints = source.getAction(resultIndex, DatabaseDataConstraintAction); + const resultColumn = data.getColumn(key.column); + const currentConstraint = resultColumn ? constraints.get(resultColumn.position) : undefined; + + return !currentConstraint || !isFilterConstraint(currentConstraint); } - const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); - const supportedOperations = data.getColumnOperations(context.data.key.column); + if (action === ACTION_DATA_GRID_FILTERS_RESET_ALL) { + const constraints = source.getAction(resultIndex, DatabaseDataConstraintAction); + return constraints.filterConstraints.length === 0 && !model.requestInfo.requestFilter; + } + + return true; + }, + + getActionInfo(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const data = source.getAction(resultIndex, ResultSetDataAction); + const resultColumn = data.getColumn(key.column); + + if (action === ACTION_DELETE) { + return { + ...action.info, + icon: 'filter-reset', + label: `Delete filter for "${resultColumn?.name ?? '?'}"`, + }; + } - return supportedOperations.length === 0; + return action.info; }, - panel: new ComputedContextMenuModel({ - id: 'clipboardValuePanel', - menuItemsGetter: context => { - if (context.contextType !== DataGridContextMenuService.cellContext) { - return []; + handler: async (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const data = source.getAction(resultIndex, ResultSetDataAction); + + if (action === ACTION_DELETE) { + const constraints = source.getAction(resultIndex, DatabaseDataConstraintAction); + const resultColumn = data.getColumn(key.column); + + if (!resultColumn) { + throw new Error(`Failed to get result column info for the following column index: "${key.column.index}"`); } - const valueGetter = () => this.clipboardService.clipboardValue || ''; - const items = this.getGeneralizedMenuItems(context, valueGetter, 'filter-clipboard', () => this.clipboardService.state === 'prompt'); + await model.request(() => { + constraints.deleteFilter(resultColumn.position); + }); + } - return [ - { - id: 'permission', - isPresent: () => true, - isHidden: () => this.clipboardService.state !== 'prompt', - isDisabled(context) { - return context.data.model.isLoading(); - }, - title: 'data_grid_table_context_menu_filter_clipboard_permission', - icon: 'permission', - onClick: async () => { - await this.clipboardService.read(); - }, - }, - ...items, - ]; - }, - }), + if (action === ACTION_DATA_GRID_FILTERS_RESET_ALL) { + const constraints = source.getAction(resultIndex, DatabaseDataConstraintAction); + + await model.request(() => { + constraints.deleteDataFilters(); + }); + } + }, }); - this.dataGridContextMenuService.add(this.getMenuFilterToken(), { - id: 'cellValue', - order: 1, - title: 'data_grid_table_filter_cell_value', - icon: 'filter', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; + + this.menuService.addCreator({ + menus: [MENU_DATA_GRID_FILTERS_CELL_VALUE], + isApplicable: context => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const data = source.getAction(resultIndex, ResultSetDataAction); + + if (model.isDisabled(resultIndex)) { + return false; + } + + const supportedOperations = data.getColumnOperations(key.column); + return supportedOperations.length > 0; }, - isHidden: context => { - const { model, resultIndex, key } = context.data; - const data = model.source.getAction(resultIndex, ResultSetDataAction); - const format = model.source.getAction(resultIndex, ResultSetFormatAction); + getItems: (context, items) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const format = source.getAction(resultIndex, ResultSetFormatAction); + const data = source.getAction(resultIndex, ResultSetDataAction); + + const cellValue = format.getText(key); const supportedOperations = data.getColumnOperations(key.column); - const value = data.getCellValue(key); + const columnLabel = data.getColumn(key.column)?.label || ''; - return value === undefined || supportedOperations.length === 0 || format.isNull(value); + const filters = supportedOperations + .filter(operation => !nullOperationsFilter(operation)) + .map(operation => { + const wrappedValue = wrapOperationArgument(operation.id, cellValue); + const clippedValue = replaceMiddle(wrappedValue, ' ... ', 8, 30); + + return new MenuBaseItem( + { + id: operation.id, + label: `${columnLabel} ${operation.expression} ${clippedValue}`, + icon: 'filter', + }, + { + onSelect: async () => { + await this.applyFilter( + model as unknown as IDatabaseDataModel, + resultIndex, + key.column, + operation.id, + wrappedValue, + ); + }, + }, + ); + }); + + return [...items, ...filters]; }, - 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 items = this.getGeneralizedMenuItems(context, cellValue, 'filter'); - return items; - }, - }), }); - this.dataGridContextMenuService.add(this.getMenuFilterToken(), { - id: 'customValue', - order: 2, - title: 'data_grid_table_filter_custom_value', - icon: 'filter-custom', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden: context => { - const { model, resultIndex, key } = context.data; - const data = model.source.getAction(resultIndex, ResultSetDataAction); - const cellValue = data.getCellValue(key); + + this.menuService.addCreator({ + menus: [MENU_DATA_GRID_FILTERS_CUSTOM], + isApplicable(context) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const data = source.getAction(resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(key.column); + const cellValue = data.getCellValue(key); - return cellValue === undefined || supportedOperations.length === 0; + return cellValue !== undefined && supportedOperations.length > 0; }, - panel: new ComputedContextMenuModel({ - 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 - .filter(operation => !nullOperationsFilter(operation)) - .map(operation => { - const title = `${columnLabel} ${operation.expression}`; + getItems: (context, items) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const data = source.getAction(resultIndex, ResultSetDataAction); + const format = source.getAction(resultIndex, ResultSetFormatAction); - return { + const supportedOperations = data.getColumnOperations(key.column); + const columnLabel = data.getColumn(key.column)?.label || ''; + const displayString = format.getText(key); + + const filters = supportedOperations + .filter(operation => !nullOperationsFilter(operation)) + .map(operation => { + const title = `${columnLabel} ${operation.expression}`; + + return new MenuBaseItem( + { id: operation.id, - isPresent: () => true, - isDisabled(context) { - return context.data.model.isLoading(); - }, - title: title + ' ..', + label: title + ' ..', icon: 'filter-custom', - onClick: async () => { - const stringifyCellValue = format.toDisplayString(cellValue); + }, + { + onSelect: async () => { const customValue = await this.commonDialogService.open(FilterCustomValueDialog, { - defaultValue: stringifyCellValue, + defaultValue: displayString, inputTitle: title + ':', }); @@ -283,116 +334,108 @@ export class DataGridContextMenuFilterService { return; } - await this.applyFilter(model, resultIndex, key.column, operation.id, customValue); + await this.applyFilter( + model as unknown as IDatabaseDataModel, + resultIndex, + key.column, + operation.id, + customValue, + ); }, - }; - }); - }, - }), - }); - this.dataGridContextMenuService.add(this.getMenuFilterToken(), { - id: 'isNullValue', - order: 3, - icon: 'filter', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden: context => { - const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); - const supportedOperations = data.getColumnOperations(context.data.key.column); + }, + ); + }); - return !supportedOperations.some(operation => operation.id === IS_NULL_ID); - }, - titleGetter: context => { - const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); - const columnLabel = data.getColumn(context.data.key.column)?.label || ''; - return `${columnLabel} IS NULL`; - }, - onClick: async context => { - await this.applyFilter(context.data.model, context.data.resultIndex, context.data.key.column, IS_NULL_ID); + return [...items, ...filters]; }, }); - this.dataGridContextMenuService.add(this.getMenuFilterToken(), { - id: 'isNotNullValue', - order: 4, - icon: 'filter', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden: context => { - const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); - const supportedOperations = data.getColumnOperations(context.data.key.column); - return !supportedOperations.some(operation => operation.id === IS_NOT_NULL_ID); - }, - titleGetter: context => { - const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); - const columnLabel = data.getColumn(context.data.key.column)?.label || ''; - return `${columnLabel} IS NOT NULL`; - }, - onClick: async context => { - await this.applyFilter(context.data.model, context.data.resultIndex, context.data.key.column, IS_NOT_NULL_ID); + this.menuService.setHandler({ + id: 'data-grid-filters-clipboard-handler', + menus: [MENU_DATA_GRID_FILTERS_CLIPBOARD], + handler: () => { + if (this.clipboardService.state === 'granted') { + this.clipboardService.read(); + } }, }); - this.dataGridContextMenuService.add(this.getMenuFilterToken(), { - id: 'deleteFilter', - order: 5, - icon: 'filter-reset', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden: context => { - const { model, resultIndex, key } = context.data; - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - const data = model.source.getAction(resultIndex, ResultSetDataAction); - const resultColumn = data.getColumn(key.column); - const currentConstraint = resultColumn ? constraints.get(resultColumn.position) : undefined; - return !currentConstraint || !isFilterConstraint(currentConstraint); - }, - titleGetter: context => { - const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); - const columnLabel = data.getColumn(context.data.key.column)?.name || ''; - return `Delete filter for ${columnLabel}`; + this.menuService.addCreator({ + menus: [MENU_DATA_GRID_FILTERS_CLIPBOARD], + isApplicable: context => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const data = source.getAction(resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(key.column); + + return this.clipboardService.clipboardAvailable && this.clipboardService.state !== 'denied' && supportedOperations.length > 0; }, - onClick: async context => { - const { model, resultIndex, key } = context.data; - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - const data = model.source.getAction(resultIndex, ResultSetDataAction); - const resultColumn = data.getColumn(key.column); + getItems: (context, items) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; - if (!resultColumn) { - throw new Error(`Failed to get result column info for the following column index: "${key.column.index}"`); + const source = model.source as unknown as ResultSetDataSource; + const data = source.getAction(resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(key.column); + const columnLabel = data.getColumn(key.column)?.label || ''; + + const result = [...items]; + + if (this.clipboardService.state === 'prompt') { + const permission = new MenuBaseItem( + { + id: 'permission', + hidden: this.clipboardService.state !== 'prompt', + label: 'data_grid_table_context_menu_filter_clipboard_permission', + icon: 'permission', + }, + { + onSelect: async () => { + await this.clipboardService.read(); + }, + }, + { isDisabled: () => model.isLoading() }, + ); + + result.push(permission); } - await model.requestDataAction(async () => { - constraints.deleteFilter(resultColumn.position); - await model.request(true); - }); - }, - }); - this.dataGridContextMenuService.add(this.getMenuFilterToken(), { - id: 'deleteAllFilters', - order: 6, - icon: 'filter-reset-all', - title: 'data_grid_table_filter_reset_all_filters', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden: context => { - const { model, resultIndex } = context.data; - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + if (this.clipboardService.state === 'granted') { + const filters = supportedOperations + .filter(operation => !nullOperationsFilter(operation)) + .map(operation => { + const val = this.clipboardService.clipboardValue || ''; + const wrappedValue = wrapOperationArgument(operation.id, val); + const clippedValue = replaceMiddle(wrappedValue, ' ... ', 8, 30); + const label = `${columnLabel} ${operation.expression} ${clippedValue}`; + + return new MenuBaseItem( + { id: operation.id, icon: 'filter-clipboard', label }, + { + onSelect: async () => { + const wrappedValue = wrapOperationArgument(operation.id, val); + + await this.applyFilter( + model as unknown as IDatabaseDataModel, + resultIndex, + key.column, + operation.id, + wrappedValue, + ); + }, + }, + { isDisabled: () => model.isLoading() }, + ); + }); - return constraints.filterConstraints.length === 0 && !model.requestInfo.requestFilter; - }, - onClick: async context => { - const { model, resultIndex } = context.data; - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - - await model.requestDataAction(async () => { - constraints.deleteDataFilters(); - await model.request(true); - }); + result.push(...filters); + } + + return result; }, }); } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.m.css deleted file mode 100644 index 30002cba9d..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.m.css +++ /dev/null @@ -1,5 +0,0 @@ -.footer { - align-items: center; - justify-content: flex-end; - gap: 24px; -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.module.css new file mode 100644 index 0000000000..85d02143ef --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.footer { + align-items: center; + justify-content: flex-end; + gap: 24px; +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx index 14192c0b75..d27df716f0 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -23,11 +23,11 @@ import { useService } from '@cloudbeaver/core-di'; import type { DialogComponent, DialogComponentProps } from '@cloudbeaver/core-dialogs'; import { ClipboardService } from '@cloudbeaver/core-ui'; -import style from './FilterCustomValueDialog.m.css'; +import style from './FilterCustomValueDialog.module.css'; interface IPayload { inputTitle: string; - defaultValue: string | number; + defaultValue: string; } export const FilterCustomValueDialog: DialogComponent = observer(function FilterCustomValueDialog({ @@ -39,7 +39,7 @@ export const FilterCustomValueDialog: DialogComponent const styles = useS(style); const inputRef = useRef(null); - const [value, setValue] = useState(payload.defaultValue); + const [value, setValue] = useState(payload.defaultValue); const handleApply = useCallback(() => resolveDialog(value), [value, resolveDialog]); const translate = useTranslate(); @@ -67,14 +67,14 @@ export const FilterCustomValueDialog: DialogComponent {clipboardService.clipboardAvailable && clipboardService.state !== 'denied' && ( - )} - - diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS.ts new file mode 100644 index 0000000000..be05b06001 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_DATA_GRID_FILTERS = createMenu('data-grid-filters', { label: 'data_grid_table_filter', icon: 'filter' }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS_CELL_VALUE.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS_CELL_VALUE.ts new file mode 100644 index 0000000000..5c223c3647 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS_CELL_VALUE.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_DATA_GRID_FILTERS_CELL_VALUE = createMenu('data-grid-filters-cell-value', { + label: 'data_grid_table_filter_cell_value', + icon: 'filter', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS_CLIPBOARD.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS_CLIPBOARD.ts new file mode 100644 index 0000000000..3bda30534d --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS_CLIPBOARD.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_DATA_GRID_FILTERS_CLIPBOARD = createMenu('data-grid-filters-clipboard', { label: 'ui_clipboard', icon: 'filter-clipboard' }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS_CUSTOM.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS_CUSTOM.ts new file mode 100644 index 0000000000..b5b9e45d85 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/MENU_DATA_GRID_FILTERS_CUSTOM.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_DATA_GRID_FILTERS_CUSTOM = createMenu('data-grid-filters-custom', { + label: 'data_grid_table_filter_custom_value', + icon: 'filter-custom', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts index 4f24ca586b..3660af390f 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts @@ -1,139 +1,148 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; +import { ActionService, MenuRadioItem, MenuService } from '@cloudbeaver/core-view'; import { + DATA_CONTEXT_DV_DDM, + DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_RESULT_KEY, + DatabaseDataConstraintAction, EOrder, - IDatabaseDataModel, - IResultSetColumnKey, - Order, - ResultSetConstraintAction, + type IDatabaseDataModel, + type IDatabaseDataOptions, + type IResultSetColumnKey, + isResultSetDataModel, + isResultSetDataSource, + type Order, ResultSetDataAction, + ResultSetDataSource, } from '@cloudbeaver/plugin-data-viewer'; -import { DataGridContextMenuService } from './DataGridContextMenuService'; +import { ACTION_DATA_GRID_ORDERING_DISABLE_ALL } from '../Actions/Ordering/ACTION_DATA_GRID_ORDERING_DISABLE_ALL.js'; +import { MENU_DATA_GRID_ORDERING } from './MENU_DATA_GRID_ORDERING.js'; -@injectable() +@injectable(() => [ActionService, MenuService]) export class DataGridContextMenuOrderService { - private static readonly menuOrderToken = 'menuOrder'; + constructor( + private readonly actionService: ActionService, + private readonly menuService: MenuService, + ) {} - constructor(private readonly dataGridContextMenuService: DataGridContextMenuService) {} - - getMenuOrderToken(): string { - return DataGridContextMenuOrderService.menuOrderToken; - } - - private async changeOrder(model: IDatabaseDataModel, resultIndex: number, column: IResultSetColumnKey, order: Order) { + private async changeOrder(unknownModel: IDatabaseDataModel, resultIndex: number, column: IResultSetColumnKey, order: Order) { + const model = unknownModel as any; + if (!isResultSetDataModel(model)) { + throw new Error('Unsupported data model'); + } const data = model.source.getAction(resultIndex, ResultSetDataAction); - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const constraints = model.source.getAction(resultIndex, DatabaseDataConstraintAction); const resultColumn = data.getColumn(column); if (!resultColumn) { throw new Error(`Failed to get result column info for the following column index: "${column.index}"`); } - await model.requestDataAction(async () => { + await model.request(() => { constraints.setOrder(resultColumn.position, order, true); - await model.request(true); }); } register(): void { - this.dataGridContextMenuService.add(this.dataGridContextMenuService.getMenuToken(), { - id: this.getMenuOrderToken(), - order: 1, - title: 'data_grid_table_order', - icon: 'order-arrow-unknown', - isPanel: true, - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isHidden(context) { - const constraints = context.data.model.source.getAction(context.data.resultIndex, ResultSetConstraintAction); - return !constraints.supported || context.data.model.isDisabled(context.data.resultIndex); - }, - }); - this.dataGridContextMenuService.add(this.getMenuOrderToken(), { - id: 'asc', - type: 'radio', - title: 'ASC', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isDisabled: context => context.data.model.isLoading(), - onClick: async context => { - await this.changeOrder(context.data.model, context.data.resultIndex, context.data.key.column, EOrder.asc); - }, - isChecked: context => { - const { model, resultIndex, key } = context.data; - const data = model.source.getAction(resultIndex, ResultSetDataAction); - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - const resultColumn = data.getColumn(key.column); + this.menuService.addCreator({ + root: true, + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, DATA_CONTEXT_DV_RESULT_KEY], + isApplicable: context => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; - return !!resultColumn && constraints.getOrder(resultColumn.position) === EOrder.asc; - }, - }); - this.dataGridContextMenuService.add(this.getMenuOrderToken(), { - id: 'desc', - type: 'radio', - title: 'DESC', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isDisabled: context => context.data.model.isLoading(), - onClick: async context => { - await this.changeOrder(context.data.model, context.data.resultIndex, context.data.key.column, EOrder.desc); - }, - isChecked: context => { - const { model, resultIndex, key } = context.data; - const data = model.source.getAction(resultIndex, ResultSetDataAction); - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - const resultColumn = data.getColumn(key.column); + const source = model.source as unknown as ResultSetDataSource; + + if (!isResultSetDataSource(source)) { + return false; + } - return !!resultColumn && constraints.getOrder(resultColumn.position) === EOrder.desc; + const constraints = source.getAction(resultIndex, DatabaseDataConstraintAction); + return constraints.supported && !model.isDisabled(resultIndex); }, + getItems: (context, items) => [...items, MENU_DATA_GRID_ORDERING], }); - this.dataGridContextMenuService.add(this.getMenuOrderToken(), { - id: 'disableOrder', - type: 'radio', - title: 'data_grid_table_disable_order', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isDisabled: context => context.data.model.isLoading(), - onClick: async context => { - await this.changeOrder(context.data.model, context.data.resultIndex, context.data.key.column, null); - }, - isChecked: context => { - const { model, resultIndex, key } = context.data; - const data = model.source.getAction(resultIndex, ResultSetDataAction); - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + + this.menuService.addCreator({ + menus: [MENU_DATA_GRID_ORDERING], + getItems: (context, items) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const data = source.getAction(resultIndex, ResultSetDataAction); + const constraints = source.getAction(resultIndex, DatabaseDataConstraintAction); const resultColumn = data.getColumn(key.column); - return !!resultColumn && constraints.getOrder(resultColumn.position) === null; + const result = [...items]; + + if (resultColumn) { + for (const order of [EOrder.asc, EOrder.desc, null]) { + result.push( + new MenuRadioItem( + { + id: `data-grid-ordering-${order ? order : 'disable'}`, + label: order ? order.toUpperCase() : 'data_grid_table_disable_order', + }, + { + onSelect: async () => { + await this.changeOrder(model, resultIndex, key.column, order); + }, + }, + { isChecked: () => constraints.getOrder(resultColumn.position) === order, isDisabled: () => model.isLoading() }, + ), + ); + } + } + + return [...result, ACTION_DATA_GRID_ORDERING_DISABLE_ALL]; }, }); - this.dataGridContextMenuService.add(this.getMenuOrderToken(), { - id: 'disableOrders', - title: 'data_grid_table_disable_all_orders', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; + + this.actionService.addHandler({ + id: 'data-grid-ordering-handler', + actions: [ACTION_DATA_GRID_ORDERING_DISABLE_ALL], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, DATA_CONTEXT_DV_RESULT_KEY], + isHidden(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + if (action === ACTION_DATA_GRID_ORDERING_DISABLE_ALL) { + const source = model.source as unknown as ResultSetDataSource; + const constraints = source.getAction(resultIndex, DatabaseDataConstraintAction); + return !constraints.orderConstraints.length; + } + + return false; }, - isHidden: context => { - const constraints = context.data.model.source.getAction(context.data.resultIndex, ResultSetConstraintAction); - return !constraints.orderConstraints.length; + isDisabled: context => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + return model.isLoading(); }, - isDisabled: context => context.data.model.isLoading(), - onClick: async context => { - const constraints = context.data.model.source.getAction(context.data.resultIndex, ResultSetConstraintAction); - await context.data.model.requestDataAction(async () => { - constraints.deleteOrders(); - await context.data.model.request(true); - }); + handler: async (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const source = model.source as unknown as ResultSetDataSource; + + switch (action) { + case ACTION_DATA_GRID_ORDERING_DISABLE_ALL: { + const constraints = source.getAction(resultIndex, DatabaseDataConstraintAction); + + await model.request(() => { + constraints.deleteOrders(); + }); + break; + } + } }, }); } 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..019c1b0554 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 @@ -1,54 +1,115 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { promptForFiles } 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 { ACTION_DOWNLOAD, ACTION_UPLOAD, ActionService, MenuService } from '@cloudbeaver/core-view'; +import { + createResultSetBlobValue, + DATA_CONTEXT_DV_DDM, + DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_RESULT_KEY, + DatabaseEditChangeType, + DataViewerService, + isResultSetDataSource, + ResultSetDataContentAction, + ResultSetDataSource, + ResultSetEditAction, + ResultSetFormatAction, +} from '@cloudbeaver/plugin-data-viewer'; -import { DataGridContextMenuService } from './DataGridContextMenuService'; - -@injectable() +@injectable(() => [NotificationService, DataViewerService, ActionService, MenuService]) export class DataGridContextMenuSaveContentService { - private static readonly menuContentSaveToken = 'menuContentSave'; - - constructor(private readonly dataGridContextMenuService: DataGridContextMenuService, private readonly notificationService: NotificationService) {} - - getMenuContentSaveToken(): string { - return DataGridContextMenuSaveContentService.menuContentSaveToken; - } + constructor( + private readonly notificationService: NotificationService, + private readonly dataViewerService: DataViewerService, + private readonly actionService: ActionService, + private readonly menuService: MenuService, + ) {} register(): void { - this.dataGridContextMenuService.add(this.dataGridContextMenuService.getMenuToken(), { - id: this.getMenuContentSaveToken(), - order: 4, - title: 'ui_download', - icon: '/icons/export.svg', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; + this.menuService.addCreator({ + root: true, + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, DATA_CONTEXT_DV_RESULT_KEY], + isApplicable: context => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + return isResultSetDataSource(model.source); }, - onClick: async context => { - const content = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataContentAction); - try { - await content.downloadFileData(context.data.key); - } catch (exception: any) { - this.notificationService.logException(exception, 'data_grid_table_context_menu_save_value_error'); + getItems: (context, items) => [...items, ACTION_UPLOAD, ACTION_DOWNLOAD], + }); + + this.actionService.addHandler({ + id: 'data-grid-save-content-handler', + actions: [ACTION_UPLOAD, ACTION_DOWNLOAD], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, DATA_CONTEXT_DV_RESULT_KEY], + isHidden: (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const content = source.getAction(resultIndex, ResultSetDataContentAction); + const format = source.getAction(resultIndex, ResultSetFormatAction); + const editor = source.getAction(resultIndex, ResultSetEditAction); + + if (action === ACTION_DOWNLOAD) { + return !content.isDownloadable(key) || !this.dataViewerService.canExportData; + } + + if (action === ACTION_UPLOAD) { + return ( + !format.isBinary(key) || + model.isReadonly(resultIndex) || + (format.isReadOnly(key) && editor.getElementState(key) !== DatabaseEditChangeType.add) + ); } + + return true; }, - isHidden: context => { - const content = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataContentAction); - return !content.isDownloadable(context.data.key); + isDisabled(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const source = model.source as unknown as ResultSetDataSource; + const content = source.getAction(resultIndex, ResultSetDataContentAction); + + if (action === ACTION_DOWNLOAD || action === ACTION_UPLOAD) { + return model.isLoading() || content.isLoading(key); + } + + return false; }, - isDisabled: context => { - const content = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataContentAction); + handler: async (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; - return ( - context.data.model.isLoading() || - (!!content.activeElement && ResultSetDataKeysUtils.isElementsKeyEqual(context.data.key, content.activeElement)) - ); + const source = model.source as unknown as ResultSetDataSource; + const content = source.getAction(resultIndex, ResultSetDataContentAction); + const edit = source.getAction(resultIndex, ResultSetEditAction); + + if (action === ACTION_DOWNLOAD) { + try { + await content.downloadFileData(key); + } catch (exception: any) { + this.notificationService.logException(exception, 'data_grid_table_context_menu_save_value_error'); + } + } + + if (action === ACTION_UPLOAD) { + promptForFiles().then(files => { + const file = files?.[0] ?? undefined; + if (file) { + edit.set(key, createResultSetBlobValue(file)); + } + }); + } }, }); } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts deleted file mode 100644 index 408de50586..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts +++ /dev/null @@ -1,70 +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 { injectable } from '@cloudbeaver/core-di'; -import { ContextMenuService, IContextMenuItem, IMenuPanel } from '@cloudbeaver/core-dialogs'; -import { Executor, IExecutor } from '@cloudbeaver/core-executor'; -import type { IDatabaseDataModel, IDataPresentationActions, IDataTableActions, IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer'; - -export interface IDataGridCellMenuContext { - model: IDatabaseDataModel; - actions: IDataTableActions; - spreadsheetActions: IDataPresentationActions; - resultIndex: number; - key: IResultSetElementKey; - simple: boolean; -} - -@injectable() -export class DataGridContextMenuService { - onRootMenuOpen: IExecutor; - static cellContext = 'data-grid-cell-context-menu'; - private static readonly menuToken = 'dataGridCell'; - - constructor(private readonly contextMenuService: ContextMenuService) { - this.onRootMenuOpen = new Executor(); - } - - getMenuToken(): string { - return DataGridContextMenuService.menuToken; - } - - constructMenuWithContext( - model: IDatabaseDataModel, - actions: IDataTableActions, - spreadsheetActions: IDataPresentationActions, - resultIndex: number, - key: IResultSetElementKey, - simple: boolean, - ): IMenuPanel { - return this.contextMenuService.createContextMenu( - { - menuId: this.getMenuToken(), - contextType: DataGridContextMenuService.cellContext, - data: { model, actions, spreadsheetActions, resultIndex, key, simple }, - }, - this.getMenuToken(), - ); - } - - openMenu( - model: IDatabaseDataModel, - actions: IDataTableActions, - spreadsheetActions: IDataPresentationActions, - resultIndex: number, - key: IResultSetElementKey, - simple: boolean, - ): void { - this.onRootMenuOpen.execute({ model, actions, spreadsheetActions, resultIndex, key, simple }); - } - - add(panelId: string, menuItem: IContextMenuItem): void { - this.contextMenuService.addMenuItem(panelId, menuItem); - } - - register(): void {} -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/MENU_DATA_GRID_EDITING.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/MENU_DATA_GRID_EDITING.ts new file mode 100644 index 0000000000..245b8b4676 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/MENU_DATA_GRID_EDITING.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_DATA_GRID_EDITING = createMenu('data-grid-editing', { label: 'data_grid_table_editing', icon: 'edit' }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/MENU_DATA_GRID_ORDERING.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/MENU_DATA_GRID_ORDERING.ts new file mode 100644 index 0000000000..820a6ed24c --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/MENU_DATA_GRID_ORDERING.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_DATA_GRID_ORDERING = createMenu('data-grid-ordering', { label: 'data_grid_table_order', icon: 'order-arrow-unknown' }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridLoader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridLoader.tsx deleted file mode 100644 index f8f42c12cb..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridLoader.tsx +++ /dev/null @@ -1,18 +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 { ComplexLoader, createComplexLoader } from '@cloudbeaver/core-blocks'; -import type { IDataPresentationProps } from '@cloudbeaver/plugin-data-viewer'; - -const loader = createComplexLoader(async function loader() { - const { DataGridTable } = await import('./DataGridTable'); - return { DataGridTable }; -}); - -export const DataGridLoader: React.FC = function DataGridLoader(props) { - return {({ DataGridTable }) => }; -}; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts index 3e19ee5f8b..ea3b7ad5bd 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,7 +9,7 @@ import { createContext } from 'react'; import type { IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer'; -import type { IDraggingPosition } from '../useGridDragging'; +import type { IDraggingPosition } from '../useGridDragging.js'; export interface IDataGridSelectionContext { selectedCells: Map; @@ -17,6 +17,10 @@ export interface IDataGridSelectionContext { selectColumn: (colIdx: number, multiple: boolean) => void; selectTable: () => void; isSelected: (rowIdx: number, colIdx: number) => boolean; + getFocusedElementPosition: () => { + rowIdx: number; + columnIdx: number; + } | null; selectRange: (startPosition: IDraggingPosition, lastPosition: IDraggingPosition, multiple: boolean, temporary: boolean) => void; } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx index b89afee0ab..81d4efeb29 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,16 +10,16 @@ import { useState } from 'react'; import { useObjectRef } from '@cloudbeaver/core-blocks'; import { - IResultSetColumnKey, - IResultSetElementKey, - IResultSetRowKey, + type IResultSetColumnKey, + type IResultSetElementKey, + type IResultSetRowKey, ResultSetDataKeysUtils, ResultSetSelectAction, } from '@cloudbeaver/plugin-data-viewer'; -import type { ITableData } from '../TableDataContext'; -import type { IDraggingPosition } from '../useGridDragging'; -import type { IDataGridSelectionContext } from './DataGridSelectionContext'; +import type { ITableData } from '../TableDataContext.js'; +import type { IDraggingPosition } from '../useGridDragging.js'; +import type { IDataGridSelectionContext } from './DataGridSelectionContext.js'; interface IGridSelectionState { range: boolean; @@ -82,8 +82,8 @@ export function useGridSelectionContext(tableData: ITableData, selectionAction: if (columns.length === 0) { for (const column of props.tableData.columns) { - if (column.columnDataIndex !== null) { - rowSelection.push(column.columnDataIndex); + if (column.key !== null) { + rowSelection.push(column.key); } } } @@ -92,12 +92,12 @@ export function useGridSelectionContext(tableData: ITableData, selectionAction: for (let rowIdx = firstRowIndex; rowIdx <= lastRowIndex; rowIdx++) { const row = props.tableData.getRow(rowIdx)!; const newElements = rowSelection - .filter(element => !rowsSelection[i].some(column => ResultSetDataKeysUtils.isEqual(column.column, element))) + .filter(element => !rowsSelection[i]!.some(column => ResultSetDataKeysUtils.isEqual(column.column, element))) .map(column => ({ row, column })); temporarySelection.set( ResultSetDataKeysUtils.serialize(row), - [...rowsSelection[i], ...newElements].filter(column => { + [...rowsSelection[i]!, ...newElements].filter(column => { if (selected) { return !rowSelection.some(key => ResultSetDataKeysUtils.isEqual(key, column.column)); } @@ -129,7 +129,7 @@ export function useGridSelectionContext(tableData: ITableData, selectionAction: selectRows( startRow, lastRow, - isIndexColumnInRange ? undefined : columnsInRange.filter(column => column.columnDataIndex !== null).map(column => column.columnDataIndex!), + isIndexColumnInRange ? undefined : columnsInRange.filter(column => column.key !== null).map(column => column.key!), multiple, temporary, ); @@ -141,7 +141,7 @@ export function useGridSelectionContext(tableData: ITableData, selectionAction: state.temporarySelection.clear(); - const column = tableData.getColumn(colIdx)?.columnDataIndex ?? undefined; + const column = tableData.getColumn(colIdx)?.key ?? undefined; const selected = selectionAction.isElementSelected({ column }); @@ -158,7 +158,7 @@ export function useGridSelectionContext(tableData: ITableData, selectionAction: } function isSelected(rowIdx: number, colIdx: number) { - const column = props.tableData.getColumn(colIdx)?.columnDataIndex ?? undefined; + const column = props.tableData.getColumn(colIdx)?.key ?? undefined; const row = props.tableData.getRow(rowIdx); @@ -221,7 +221,7 @@ export function useGridSelectionContext(tableData: ITableData, selectionAction: return; } - const isIndexColumn = props.tableData.isIndexColumn(column.key); + const isIndexColumn = props.tableData.isIndexColumn(column); const row = props.tableData.getRow(cell.rowIdx); if (!row) { @@ -233,11 +233,24 @@ export function useGridSelectionContext(tableData: ITableData, selectionAction: return; } - if (column.columnDataIndex !== null) { - selectCell({ row, column: column.columnDataIndex }, multiple); + if (column.key !== null) { + selectCell({ row, column: column.key }, multiple); } } + function getFocusedElementPosition() { + const element = props.selectionAction.getFocusedElement(); + + if (!element) { + return null; + } + + const column = props.tableData.getColumnIndexFromColumnKey(element.column); + const row = props.tableData.getRowIndexFromKey(element.row); + + return { rowIdx: row, columnIdx: column }; + } + return useObjectRef( () => ({ get selectedCells() { @@ -246,6 +259,7 @@ export function useGridSelectionContext(tableData: ITableData, selectionAction: select, selectColumn, selectTable, + getFocusedElementPosition, isSelected, selectRange, }), diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.module.css new file mode 100644 index 0000000000..7d6f473f79 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.module.css @@ -0,0 +1,100 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.container { + --data-grid-edited-color: rgba(255, 153, 0, 0.3); + --data-grid-added-color: rgba(145, 255, 0, 0.3); + --data-grid-deleted-color: rgba(255, 51, 0, 0.3); + --data-grid-order-button-unordered: #c4c4c4; + --data-grid-readonly-status-color: #e28835; + --data-grid-cell-selection-background-color: rgba(150, 150, 150, 0.2); + --data-grid-cell-selection-background-color-focus: rgba(0, 145, 234, 0.2); + --data-grid-index-cell-border-color: var(--theme-primary); + --data-grid-selected-row-color: var(--theme-secondary) !important; + + composes: theme-typography--caption from global; + outline: 0; + overflow: auto; + user-select: none; + -webkit-user-select: none; + + :global(.rdg-cell-custom-edited) { + background-color: var(--data-grid-edited-color) !important; + } + + :global(.rdg-cell-custom-added) { + background-color: var(--data-grid-added-color) !important; + } + + :global(.rdg-cell-custom-deleted) { + background-color: var(--data-grid-deleted-color) !important; + } +} + +.container .grid { + width: 100%; + :global(.rdg-table-header__readonly-status) { + background-color: var(--data-grid-readonly-status-color) !important; + } + + :global(.rdg-table-header__order-button_unordered) { + color: var(--data-grid-order-button-unordered) !important; + + &:hover { + color: var(--theme-primary) !important; + } + } + + :global(.rdg-row:hover .rdg-cell), + :global(.rdg-row:hover .rdg-cell-frozen) { + border-bottom: 1px solid !important; + border-bottom-color: var(--theme-positive) !important; + } + + :global(.rdg-cell-custom-highlighted-row) { + background: var(--data-grid-selected-row-color) !important; + + &:global(.rdg-cell:first-child::before) { + position: absolute; + content: ''; + top: 0; + left: 0; + width: 2px; + height: 100%; + background-color: var(--data-grid-index-cell-border-color); + } + } + + :global(.rdg-cell-custom-selected) { + box-shadow: none !important; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: var(--data-grid-cell-selection-background-color); + } + } + + :global(.rdg-cell-custom-editing) { + box-shadow: none; + background-color: inherit; + } +} + +.grid:focus-within { + :global(.rdg-cell-custom-selected::before) { + background-color: var(--data-grid-cell-selection-background-color-focus); + } + + :global(.rdg-cell-custom-editing)::before { + background-color: transparent; + } +} 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..5dc72d11df 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -1,139 +1,100 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import styled from 'reshadow'; +import { useCallback, useLayoutEffect, useMemo, useRef, type HTMLAttributes } from 'react'; +import { reaction } from 'mobx'; -import { TextPlaceholder, useObjectRef, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; +import { getComputed, s, TextPlaceholder, useObjectRef, useS, useTranslate } from '@cloudbeaver/core-blocks'; +// import { useService } from '@cloudbeaver/core-di'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; -import { Executor } from '@cloudbeaver/core-executor'; -import { ClipboardService } from '@cloudbeaver/core-ui'; +// import { ClipboardService } from '@cloudbeaver/core-ui'; +import { useCaptureViewContext } from '@cloudbeaver/core-view'; import { - DatabaseDataSelectActionsData, + DataGrid, + useCreateGridReactiveValue, + type DataGridRef, + type ICellPosition, + type IDataGridCellRenderer, + type DataGridProps, +} from '@cloudbeaver/plugin-data-grid'; +import { + DATA_CONTEXT_DV_PRESENTATION, + type DatabaseDataSelectActionsData, DatabaseEditChangeType, - IDatabaseResultSet, - IDataPresentationProps, - IResultSetEditActionData, - IResultSetElementKey, - IResultSetPartialKey, + DatabaseSelectAction, + DataViewerPresentationType, + type IDatabaseDataModel, + type IDataPresentationProps, + type IResultSetEditActionData, + type IResultSetElementKey, + type IResultSetPartialKey, + isBooleanValuePresentationAvailable, ResultSetDataKeysUtils, + ResultSetDataSource, ResultSetSelectAction, + ResultSetViewAction, + DatabaseDataConstraintAction, + getNextOrder, + isResultSetDataModel, } from '@cloudbeaver/plugin-data-viewer'; -import DataGrid, { type DataGridHandle, type Position } from '@cloudbeaver/plugin-react-data-grid'; -import '@cloudbeaver/plugin-react-data-grid/react-data-grid-dist/lib/styles.css'; - -import { CellPosition, EditingContext } from '../Editing/EditingContext'; -import { useEditing } from '../Editing/useEditing'; -import baseStyles from '../styles/base.scss'; -import { reactGridStyles } from '../styles/styles'; -import { CellRenderer } from './CellRenderer/CellRenderer'; -import { DataGridContext, IColumnResizeInfo, IDataGridContext } from './DataGridContext'; -import { DataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext'; -import { useGridSelectionContext } from './DataGridSelection/useGridSelectionContext'; -import { CellFormatter } from './Formatters/CellFormatter'; -import { TableDataContext } from './TableDataContext'; -import { useGridDragging } from './useGridDragging'; -import { useGridSelectedCellsCopy } from './useGridSelectedCellsCopy'; -import { useTableData } from './useTableData'; - -interface IInnerState { - lastCount: number; - lastScrollTop: number; -} - -function isAtBottom(event: React.UIEvent): boolean { - const target = event.target as HTMLDivElement; - return target.clientHeight + target.scrollTop + 100 > target.scrollHeight; -} - -const rowHeight = 25; -const headerHeight = 28; - -export const DataGridTable = observer>(function DataGridTable({ + +import { CellRenderer } from './CellRenderer/CellRenderer.js'; +import { DataGridContext, type IDataGridContext } from './DataGridContext.js'; +import { DataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext.js'; +import { useGridSelectionContext } from './DataGridSelection/useGridSelectionContext.js'; +import classes from './DataGridTable.module.css'; +import { CellFormatter } from './Formatters/CellFormatter.js'; +import { TableDataContext } from './TableDataContext.js'; +import { useGridDragging } from './useGridDragging.js'; +import { useGridSelectedCellsCopy } from './useGridSelectedCellsCopy.js'; +import { useTableData } from './useTableData.js'; +import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader.js'; +import { TableIndexColumnHeader } from './TableColumnHeader/TableIndexColumnHeader.js'; + +const ROW_HEIGHT = 24; +export const HEADER_HEIGHT = 32; +export const HEADER_WITH_DESC_HEIGHT = 42; + +export const DataGridTable = observer(function DataGridTable({ model, actions, resultIndex, simple, className, + dataFormat, + ...rest }) { const translate = useTranslate(); - - const clipboardService = useService(ClipboardService); + const styles = useS(classes); const gridContainerRef = useRef(null); - const editorRef = useRef(null); const dataGridDivRef = useRef(null); - const dataGridRef = useRef(null); - const innerState = useObjectRef( - () => ({ - lastCount: 0, - lastScrollTop: 0, - }), - false, - ); - const styles = useStyles(reactGridStyles, baseStyles); - const [columnResize] = useState(() => new Executor()); - - const selectionAction = model.source.getAction(resultIndex, ResultSetSelectAction); - - const focusSyncRef = useRef(null); - - const editingContext = useEditing({ - readonly: model.isReadonly(resultIndex) || model.isDisabled(resultIndex), - onEdit: (position, code, key) => { - const column = tableData.getColumn(position.idx); - const row = tableData.getRow(position.rowIdx); - - if (!column?.columnDataIndex || !row) { - return false; - } - - const cellKey: IResultSetElementKey = { row, column: column.columnDataIndex }; - - if (tableData.isCellReadonly(cellKey)) { - return false; - } - - switch (code) { - case 'Backspace': - tableData.editor.set(cellKey, ''); - break; - case 'Enter': - break; - default: - if (key) { - if (/^[\d\p{L}]$/iu.test(key) && key.length === 1) { - tableData.editor.set(cellKey, key); - } else { - return false; - } - } - } + const focusedCell = useRef(null); + const focusSyncRef = useRef(null); + const dataGridRef = useRef(null); - return true; - }, - onCloseEditor: () => { - restoreFocus(); - }, - }); + const selectionAction = (model.source as unknown as ResultSetDataSource).getAction(resultIndex, ResultSetSelectAction); + const viewAction = (model.source as unknown as ResultSetDataSource).getAction(resultIndex, ResultSetViewAction); - const tableData = useTableData(model, resultIndex, dataGridDivRef); + const tableData = useTableData(model as unknown as IDatabaseDataModel, resultIndex, dataGridDivRef); const gridSelectionContext = useGridSelectionContext(tableData, selectionAction); - function restoreFocus() { - const gridDiv = gridContainerRef.current; - const focusSink = gridDiv?.querySelector('[tabindex="0"]'); - focusSink?.focus(); - } + const restoreFocus = useCallback( + function restoreFocus() { + const gridDiv = gridContainerRef.current; + const focusSink = gridDiv?.querySelector('[aria-selected="true"]'); + focusSink?.focus(); + }, + [gridContainerRef], + ); function isGridInFocus(): boolean { const gridDiv = gridContainerRef.current; - const focusSink = gridDiv?.querySelector('[tabindex="0"]'); + const focusSink = gridDiv?.querySelector('[aria-selected="true"]'); if (!gridDiv || !focusSink) { return false; @@ -158,18 +119,47 @@ export const DataGridTable = observer ({ - selectCell(pos: Position, scroll = false): void { - if (dataGridRef.current?.selectedCell.idx !== pos.idx || dataGridRef.current.selectedCell.rowIdx !== pos.rowIdx || scroll) { + const handlers = useObjectRef(() => ({ + selectCell(pos: ICellPosition, scroll = false): void { + if (focusedCell.current?.colIdx !== pos.colIdx || focusedCell.current?.rowIdx !== pos.rowIdx || scroll) { dataGridRef.current?.selectCell(pos); } }, + focusCell(key: Partial | null, initial = false) { + if ((!key?.column || !key?.row) && initial) { + const selectedElements = selectionAction.getSelectedElements(); + + if (selectedElements.length > 0) { + key = selectedElements[0]!; + } else { + key = { column: viewAction.columnKeys[0], row: viewAction.rowKeys[0] }; + } + selectionAction.focus(key as IResultSetElementKey); + } + + if (!key?.column || !key?.row) { + if (initial) { + focusSyncRef.current = { colIdx: 0, rowIdx: -1 }; + this.selectCell(focusSyncRef.current); + } else { + focusSyncRef.current = null; + } + return; + } + + const colIdx = tableData.getColumnIndexFromColumnKey(key.column!); + const rowIdx = tableData.getRowIndexFromKey(key.row!); + + focusSyncRef.current = { colIdx, rowIdx }; + + this.selectCell({ colIdx, rowIdx }); + }, })); - const gridSelectedCellCopy = useGridSelectedCellsCopy(tableData, selectionAction, gridSelectionContext); + const gridSelectedCellCopy = useGridSelectedCellsCopy(tableData, selectionAction as unknown as DatabaseSelectAction, gridSelectionContext); const { onMouseDownHandler, onMouseMoveHandler } = useGridDragging({ onDragStart: startPosition => { - hamdlers.selectCell({ idx: startPosition.colIdx, rowIdx: startPosition.rowIdx }); + handlers.selectCell(startPosition); }, onDragOver: (startPosition, currentPosition, event) => { gridSelectionContext.selectRange(startPosition, currentPosition, event.ctrlKey || event.metaKey, true); @@ -179,277 +169,390 @@ export const DataGridTable = observer) { - gridSelectedCellCopy.onKeydownHandler(event); - - if (EventContext.has(event, EventStopPropagationFlag) || tableData.isReadOnly() || model.isReadonly(resultIndex)) { - return; - } - - const cell = selectionAction.getFocusedElement(); - const activeElements = selectionAction.getActiveElements(); - const activeRows = selectionAction.getActiveRows(); - - if (!cell) { - return; - } - - const idx = tableData.getColumnIndexFromColumnKey(cell.column); - const rowIdx = tableData.getRowIndexFromKey(cell.row); - const position: CellPosition = { idx, rowIdx }; - - if (editingContext.isEditing(position)) { - return; - } - - switch (event.nativeEvent.code) { - case 'Escape': { - tableData.editor.revert(...activeElements); - return; - } - case 'Insert': { - if (event.altKey) { - if (event.ctrlKey || event.metaKey) { - tableData.editor.duplicate(...activeRows); - } else { - tableData.editor.add(cell); - } - return; - } - } - } - - const editingState = tableData.editor.getElementState(cell); - - switch (event.nativeEvent.code) { - case 'Delete': { - const filteredRows = activeRows.filter(cell => tableData.editor.getElementState(cell) !== DatabaseEditChangeType.delete); - - if (filteredRows.length > 0) { - const editor = tableData.editor; - const firstRow = filteredRows[0]; - const editingState = tableData.editor.getElementState(firstRow); - - editor.delete(...filteredRows); - - if (editingState === DatabaseEditChangeType.add) { - if (rowIdx - 1 > 0) { - hamdlers.selectCell({ idx, rowIdx: rowIdx - 1 }); - } - } else { - if (rowIdx + 1 < tableData.rows.length) { - hamdlers.selectCell({ idx, rowIdx: rowIdx + 1 }); - } - } - } - - return; - } - case 'KeyV': { - if (editingState === DatabaseEditChangeType.delete) { - return; - } - - if (event.ctrlKey || event.metaKey) { - if (!clipboardService.clipboardAvailable || clipboardService.state === 'denied' || tableData.isCellReadonly(cell)) { - return; - } - - clipboardService - .read() - .then(value => tableData.editor.set(cell, value)) - .catch(); - return; - } - } - } - - if (editingState === DatabaseEditChangeType.delete) { - return; - } - - editingContext.edit({ idx, rowIdx }, event.nativeEvent.code, event.key); - } + useCaptureViewContext((context, id) => { + context.set(DATA_CONTEXT_DV_PRESENTATION, { type: DataViewerPresentationType.Data }, id); + }); - useEffect(() => { + useLayoutEffect(() => { function syncEditor(data: IResultSetEditActionData) { const editor = tableData.editor; if (data.resultId !== editor.result.id || !data.value || data.value.length === 0 || data.type === DatabaseEditChangeType.delete) { return; } - const key = data.value[data.value.length - 1].key; + const key = data.value[data.value.length - 1]!.key; - const idx = tableData.getColumnIndexFromColumnKey(key.column); + const colIdx = tableData.getColumnIndexFromColumnKey(key.column); const rowIdx = tableData.getRowIndexFromKey(key.row); - if (data.revert) { - editingContext.closeEditor({ - rowIdx, - idx, - }); - } - if (selectionAction.isFocused(key)) { - const rowTop = rowIdx * rowHeight; - const gridDiv = dataGridDivRef.current; - dataGridRef.current?.scrollToCell({ idx }); - - if (gridDiv) { - if (rowTop < gridDiv.scrollTop - rowHeight + headerHeight) { - gridDiv.scrollTo({ - top: rowTop, - }); - } else if (rowTop > gridDiv.scrollTop + gridDiv.clientHeight - headerHeight - rowHeight) { - gridDiv.scrollTo({ - top: rowTop - gridDiv.clientHeight + headerHeight + rowHeight, - }); - } - } + dataGridRef.current?.scrollToCell({ colIdx }); return; } - hamdlers.selectCell({ idx, rowIdx }); + handlers.selectCell({ colIdx, rowIdx }); } tableData.editor.action.addHandler(syncEditor); function syncFocus(data: DatabaseDataSelectActionsData) { - setTimeout(() => { - // TODO: update focus after render rows update - if (data.type === 'focus') { - if (!data.key?.column || !data.key.row) { - return; - } - - const idx = tableData.getColumnIndexFromColumnKey(data.key.column); - const rowIdx = tableData.getRowIndexFromKey(data.key.row); - - focusSyncRef.current = { idx, rowIdx }; - - hamdlers.selectCell({ idx, rowIdx }); - } - }, 1); + if (data.type === 'focus') { + // TODO: we need this delay to update focus after render rows update + setTimeout(() => { + handlers.focusCell(data.key); + setTimeout(() => restoreFocus(), 1); + }, 1); + } } selectionAction.actions.addHandler(syncFocus); + handlers.focusCell(selectionAction.getFocusedElement(), true); return () => { tableData.editor.action.removeHandler(syncEditor); }; - }, [tableData.editor, editingContext, selectionAction]); - - useEffect(() => { - const gridDiv = dataGridDivRef.current; - - if ( - gridDiv && - innerState.lastCount > model.source.count && - model.source.count * rowHeight < gridDiv.scrollTop + gridDiv.clientHeight - headerHeight - ) { - gridDiv.scrollTo({ - top: model.source.count * rowHeight - gridDiv.clientHeight + headerHeight - 1, - }); - } + }, [tableData.editor, selectionAction, handlers, tableData, restoreFocus]); - innerState.lastCount = model.source.count; - }, [model.source.count]); + const handleFocusChange = (position: ICellPosition) => { + focusedCell.current = position; + const columnIndex = position.colIdx; + const rowIndex = position.rowIdx; - const handleFocusChange = (position: CellPosition) => { - if (focusSyncRef.current && focusSyncRef.current.idx === position.idx && focusSyncRef.current.rowIdx === position.rowIdx) { + if (focusSyncRef.current && focusSyncRef.current.colIdx === columnIndex && focusSyncRef.current.rowIdx === rowIndex) { focusSyncRef.current = null; return; } - const column = tableData.getColumn(position.idx); - const row = tableData.getRow(position.rowIdx); + const column = tableData.getColumn(columnIndex); + const row = tableData.getRow(rowIndex); - if (column?.columnDataIndex && row) { + if (column?.key && row) { selectionAction.focus({ row, - column: { ...column.columnDataIndex }, + column: { ...column.key }, }); + } else { + selectionAction.focus(null); } }; - const handleScroll = useCallback( - async (event: React.UIEvent) => { - const target = event.target as HTMLDivElement; - const toBottom = target.scrollTop > innerState.lastScrollTop; - - innerState.lastScrollTop = target.scrollTop; - - if (!toBottom || !isAtBottom(event)) { - return; - } - - const result = model.getResult(resultIndex); - if (result?.loadedFully) { - return; - } + const handleScrollToBottom = useCallback(async () => { + const result = model.source.getResult(resultIndex); + if (result?.loadedFully) { + return; + } - await model.requestDataPortion(0, model.countGain + model.source.count); - }, - [model, resultIndex], - ); + await model.requestDataPortion(0, model.countGain + model.source.count); + }, [model, resultIndex]); const gridContext = useMemo( () => ({ model, actions, - columnResize, resultIndex, simple, isGridInFocus, - getEditorPortal: () => editorRef.current, getDataGridApi: () => dataGridRef.current, focus: restoreFocus, }), - [model, actions, resultIndex, simple, editorRef, dataGridRef, gridContainerRef, restoreFocus], + [model, actions, resultIndex, simple, dataGridRef, restoreFocus], + ); + + const columnsCount = useCreateGridReactiveValue( + () => tableData.columns.length, + onValueChange => reaction(() => tableData.columns.length, onValueChange), + [tableData], + ); + const rowsCount = useCreateGridReactiveValue( + () => tableData.rows.length, + onValueChange => reaction(() => tableData.rows.length, onValueChange), + [tableData], ); + const headerHeight = getComputed(() => (tableData.hasDescription ? HEADER_WITH_DESC_HEIGHT : HEADER_HEIGHT)); + + function getCell(rowIdx: number, colIdx: number) { + return ; + } + const cell = useCreateGridReactiveValue(getCell, (onValueChange, rowIdx, colIdx) => reaction(() => getCell(rowIdx, colIdx), onValueChange), []); + + function getCellText(rowIdx: number, colIdx: number) { + const row = tableData.rows[rowIdx]; + const column = tableData.getColumn(colIdx)?.key; + + if (!row || !column) { + return ''; + } + + return tableData.format.getText({ row, column }); + } + + const cellText = useCreateGridReactiveValue( + getCellText, + (onValueChange, rowIdx, colIdx) => reaction(() => getCellText(rowIdx, colIdx), onValueChange), + [tableData], + ); + + function getHeaderWidth(colIdx: number) { + if (colIdx === 0) { + return 60; + } + return null; + } + + function getHeaderPinned(colIdx: number) { + if (colIdx === 0) { + return true; + } + return false; + } + + function getHeaderResizable(colIdx: number) { + return colIdx !== 0; + } + + function getHeaderElement(colIdx: number) { + const column = tableData.getColumn(colIdx); + + if (!column) { + return null; + } + + if (tableData.isIndexColumn(column)) { + return ; + } + + return ; + } + + const headerElement = useCreateGridReactiveValue( + getHeaderElement, + (onValueChange, colIdx) => reaction(() => getHeaderElement(colIdx), onValueChange), + [tableData], + ); + + function getCellElement(rowIdx: number, colIdx: number, props: HTMLAttributes, renderDefaultCell: IDataGridCellRenderer) { + return ; + } + + const cellElement = useCreateGridReactiveValue( + getCellElement, + (onValueChange, rowIdx, colIdx, props, renderDefaultCell) => + reaction(() => getCellElement(rowIdx, colIdx, props, renderDefaultCell), onValueChange), + [], + ); + + function getColumnSortable(colIdx: number) { + if (!isResultSetDataModel(model)) { + return false; + } + const constraintsAction = (model.source as unknown as ResultSetDataSource).tryGetAction(resultIndex, DatabaseDataConstraintAction); + return ( + Boolean(tableData.getColumn(colIdx) && constraintsAction?.supported && isResultSetDataModel(model) && !model.isDisabled(resultIndex)) && + colIdx !== 0 + ); + } + + const columnSortable = useCreateGridReactiveValue( + getColumnSortable, + (onValueChange, colIdx) => reaction(() => getColumnSortable(colIdx), onValueChange), + [tableData, model], + ); + + function getColumnSortingState(colIdx: number) { + if (!isResultSetDataModel(model)) { + return null; + } + const constraintsAction = (model.source as unknown as ResultSetDataSource).tryGetAction(resultIndex, DatabaseDataConstraintAction); + const column = tableData.getColumn(colIdx)?.key; + if (!column || !constraintsAction?.supported) { + return null; + } + const resultColumn = tableData.getColumnInfo(column); + return resultColumn ? constraintsAction?.getOrder(resultColumn.position) : null; + } + + const columnSortingState = useCreateGridReactiveValue( + getColumnSortingState, + (onValueChange, colIdx) => reaction(() => getColumnSortingState(colIdx), onValueChange), + [tableData, model], + ); + + function handleSort(colIdx: number, order: 'asc' | 'desc' | null, isMultiple: boolean) { + const column = tableData.getColumn(colIdx)?.key; + if (!column) { + return; + } + const resultColumn = tableData.getColumnInfo(column); + if (!resultColumn) { + return; + } + const constraintsAction = (model.source as unknown as ResultSetDataSource).tryGetAction(resultIndex, DatabaseDataConstraintAction); + const currentOrder = constraintsAction!.getOrder(resultColumn.position); + const nextOrder = getNextOrder(currentOrder); + model.request(() => { + constraintsAction!.setOrder(resultColumn.position, nextOrder, isMultiple); + }); + } + + function handleCellChange(rowIdx: number, colIdx: number, value: string) { + const row = tableData.rows[rowIdx]; + const column = tableData.getColumn(colIdx)?.key; + + if (!row || !column) { + return; + } + + tableData.editor.set({ row, column }, value); + } + + function isCellEditable(rowIdx: number, colIdx: number): boolean { + const row = tableData.rows[rowIdx]; + const column = tableData.getColumn(colIdx)?.key; + + if (!row || !column) { + return false; + } + + const cell = { row, column }; + + const editionState = tableData.getEditionState(cell); + + if (!gridContext.model.hasElementIdentifier(tableData.view.resultIndex) && editionState !== DatabaseEditChangeType.add) { + return false; + } + + if (tableData.format.isBinary(cell) || tableData.format.isGeometry(cell) || tableData.dataContent.isTextTruncated(cell)) { + return false; + } + + const resultColumn = tableData.getColumnInfo(cell.column); + const value = tableData.getCellValue(cell); + + if (!resultColumn || value === undefined) { + return false; + } + + const handleByBooleanFormatter = isBooleanValuePresentationAvailable(value, resultColumn); + + return !(handleByBooleanFormatter || tableData.isCellReadonly(cell)); + } + + function getColumnKey(colIdx: number) { + const column = tableData.columns[colIdx]; + + if (column?.key) { + return ResultSetDataKeysUtils.serialize(column.key); + } + + return `_${String(colIdx)}`; + } if (!tableData.columns.length) { return {translate('data_grid_table_empty_placeholder')}; } - return styled(styles)( + const handleCellKeyDown: DataGridProps['onCellKeyDown'] = ({ rowIdx, colIdx }, event) => { + gridSelectedCellCopy.onKeydownHandler(event); + const cell = selectionAction.getFocusedElement(); + + if (EventContext.has(event, EventStopPropagationFlag) || model.isReadonly(resultIndex) || !cell) { + return; + } + + // we can't edit table cells if table doesn't have row identifier, but we can edit just added/duplicated rows before insert (CB-6063) + const canEdit = model.hasElementIdentifier(resultIndex) || tableData.editor.getElementState(cell) === DatabaseEditChangeType.add; + const activeElements = selectionAction.getActiveElements(); + const activeRows = selectionAction.getActiveRows(); + + switch (event.code) { + case 'Escape': { + if (!canEdit) { + return; + } + tableData.editor.revert(...activeElements); + return; + } + case 'KeyR': { + if (event.altKey) { + if (event.shiftKey) { + tableData.editor.duplicate(...activeRows); + } else { + tableData.editor.add(cell); + } + return; + } + return; + } + case 'Delete': { + if (!canEdit) { + return; + } + event.preventGridDefault(); + + const filteredRows = activeRows.filter(cell => tableData.editor.getElementState(cell) !== DatabaseEditChangeType.delete); + + if (filteredRows.length > 0) { + const editor = tableData.editor; + const firstRow = filteredRows[0]!; + const editingState = tableData.editor.getElementState(firstRow); + + editor.delete(...filteredRows); + + if (editingState === DatabaseEditChangeType.add) { + if (rowIdx - 1 > 0) { + handlers.selectCell({ colIdx, rowIdx: rowIdx - 1 }); + } + } else { + if (rowIdx + 1 < tableData.rows.length) { + handlers.selectCell({ colIdx, rowIdx: rowIdx + 1 }); + } + } + } + } + } + }; + + return ( - - - - , - }} - rows={tableData.rows} - rowKeyGetter={ResultSetDataKeysUtils.serialize} - headerRowHeight={headerHeight} - rowHeight={rowHeight} - renderers={{ - renderCell: (key, props) => , - }} - onSelectedCellChange={handleFocusChange} - onColumnResize={(idx, width) => columnResize.execute({ column: idx, width })} - onScroll={handleScroll} - /> -
- - - + +
+ headerHeight} + getHeaderWidth={getHeaderWidth} + getHeaderPinned={getHeaderPinned} + getHeaderResizable={getHeaderResizable} + getRowHeight={() => ROW_HEIGHT} + getColumnKey={getColumnKey} + columnCount={columnsCount} + rowCount={rowsCount} + columnSortable={columnSortable} + columnSortingState={columnSortingState} + getRowId={rowIdx => (tableData.rows[rowIdx] ? ResultSetDataKeysUtils.serialize(tableData.rows[rowIdx]) : '')} + onFocus={handleFocusChange} + onScrollToBottom={handleScrollToBottom} + onColumnSort={handleSort} + onCellChange={handleCellChange} + onCellKeyDown={handleCellKeyDown} + onHeaderKeyDown={gridSelectedCellCopy.onKeydownHandler} + /> +
+
- , + ); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTableLoader.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTableLoader.ts new file mode 100644 index 0000000000..a71bd76811 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTableLoader.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const DataGridTable = importLazyComponent(() => import('./DataGridTable.js').then(m => m.DataGridTable)); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.m.css deleted file mode 100644 index b8ec0b4eff..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.m.css +++ /dev/null @@ -1,26 +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. - */ - -.wrapper { - height: 100%; - display: flex; - overflow: hidden; - box-sizing: border-box; -} - -.container { - flex: 1; - overflow: hidden; -} - -.menuContainer { - width: 25px; - height: 100%; - box-sizing: border-box; - overflow: hidden; -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.module.css new file mode 100644 index 0000000000..3769a321dd --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.module.css @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.wrapper { + display: flex; + overflow: hidden; + box-sizing: border-box; + gap: 6px; + height: 100%; +} + +.container { + flex: 1; + overflow: hidden; + display: flex; + align-items: center; +} + +.menuContainer { + display: flex; + box-sizing: border-box; + overflow: hidden; + + &:empty { + display: none; + } +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx index 3a11d52dfa..f15d2ef3d0 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx @@ -1,65 +1,75 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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, useState } from 'react'; +import { use, useContext } from 'react'; +import { DataGridCellInnerContext } from '@cloudbeaver/plugin-data-grid'; import { getComputed, s, useObjectRef, useS } from '@cloudbeaver/core-blocks'; -import type { IDataPresentationActions, IResultSetElementKey, IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer'; -import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid'; +import type { IDataPresentationActions, IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer'; -import { EditingContext } from '../../Editing/EditingContext'; -import { CellContext } from '../CellRenderer/CellContext'; -import { DataGridContext } from '../DataGridContext'; -import { TableDataContext } from '../TableDataContext'; -import style from './CellFormatter.m.css'; -import { CellFormatterFactory } from './CellFormatterFactory'; -import { CellMenu } from './Menu/CellMenu'; +import { CellContext } from '../CellRenderer/CellContext.js'; +import { DataGridContext } from '../DataGridContext.js'; +import { TableDataContext } from '../TableDataContext.js'; +import style from './CellFormatter.module.css'; +import { CellFormatterFactory } from './CellFormatterFactory.js'; +import { CellMenu } from './Menu/CellMenu.js'; -interface Props extends RenderCellProps { - className?: string; +interface Props { + rowIdx: number; + colIdx: number; } -export const CellFormatter = observer(function CellFormatter({ className, ...rest }) { +export const CellFormatter = observer(function CellFormatter({ rowIdx, colIdx }) { const context = useContext(DataGridContext); const tableDataContext = useContext(TableDataContext); + const innerCellContext = use(DataGridCellInnerContext); const cellContext = useContext(CellContext); - const editingContext = useContext(EditingContext); - const [menuVisible, setMenuVisible] = useState(false); - const isEditing = cellContext.isEditing; - const showCellMenu = getComputed(() => !isEditing && (cellContext.isFocused || cellContext.mouse.state.mouseEnter || menuVisible)); + + const cell = cellContext.cell; + const showCellMenu = getComputed( + () => !!cell && (innerCellContext?.isFocused || cellContext.isFocused || cellContext.isHovered || cellContext.isMenuVisible), + ); const styles = useS(style); const spreadsheetActions = useObjectRef>({ edit(position) { - const idx = tableDataContext.getColumnIndexFromColumnKey(position.column); + const colIdx = tableDataContext.getColumnIndexFromColumnKey(position.column); const rowIdx = tableDataContext.getRowIndexFromKey(position.row); - if (idx !== -1) { - editingContext.edit({ idx, rowIdx }); + if (colIdx !== -1) { + context.getDataGridApi()?.openEditor({ colIdx, rowIdx }); } }, }); + function handleCellMenuStateSwitch(visible: boolean): void { + if (cellContext.isMenuVisible && !visible) { + cellContext.setHover(false); + } + + cellContext.setMenuVisibility(visible); + } + return ( -
+
- +
- {showCellMenu && cellContext.cell && ( + {showCellMenu && (
)} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx index 397149fd59..388975f849 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,37 +8,42 @@ import { observer } from 'mobx-react-lite'; import { useContext, useRef } from 'react'; -import { IResultSetRowKey, isBooleanValuePresentationAvailable } from '@cloudbeaver/plugin-data-viewer'; -import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid'; +import { isBooleanValuePresentationAvailable } from '@cloudbeaver/plugin-data-viewer'; -import { CellContext } from '../CellRenderer/CellContext'; -import { TableDataContext } from '../TableDataContext'; -import { BooleanFormatter } from './CellFormatters/BooleanFormatter'; -import { TextFormatter } from './CellFormatters/TextFormatter'; +import { CellContext } from '../CellRenderer/CellContext.js'; +import { TableDataContext } from '../TableDataContext.js'; +import { BlobFormatter } from './CellFormatters/BlobFormatter.js'; +import { BooleanFormatter } from './CellFormatters/BooleanFormatter.js'; +import { TextFormatter } from './CellFormatters/TextFormatter.js'; +import type { ICellFormatterProps } from './ICellFormatterProps.js'; +import { IndexFormatter } from './IndexFormatter.js'; -interface IProps extends RenderCellProps { - isEditing: boolean; -} - -export const CellFormatterFactory = observer(function CellFormatterFactory(props) { - const formatterRef = useRef> | null>(null); +export const CellFormatterFactory = observer(function CellFormatterFactory(props) { + const formatterRef = useRef | null>(null); const tableDataContext = useContext(TableDataContext); const cellContext = useContext(CellContext); - if (!props.isEditing || formatterRef.current === null) { + if (formatterRef.current === null) { 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; + } } } + } else { + formatterRef.current = IndexFormatter; } } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.module.css new file mode 100644 index 0000000000..115429dc56 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.module.css @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.blobFormatter { + display: flex; + align-items: center; + color: var(--theme-primary); +} 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..f0f8abbe15 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.tsx @@ -0,0 +1,42 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, useS } from '@cloudbeaver/core-blocks'; +import { isResultSetContentValue } from '@dbeaver/result-set-api'; + +import { CellContext } from '../../CellRenderer/CellContext.js'; +import { DataGridContext } from '../../DataGridContext.js'; +import { TableDataContext } from '../../TableDataContext.js'; +import style from './BlobFormatter.module.css'; +import type { ICellFormatterProps } from '../ICellFormatterProps.js'; +import { NullFormatter as GridNullFormatter, BlobFormatter as GridBlobFormatter } from '@cloudbeaver/plugin-data-grid'; + +export const BlobFormatter = observer(function BlobFormatter() { + const context = useContext(DataGridContext); + const tableDataContext = useContext(TableDataContext); + const cellContext = useContext(CellContext); + const cell = cellContext.cell; + const styles = useS(style); + + if (!context || !tableDataContext || !cell) { + return null; + } + + const formatter = tableDataContext.format; + const rawValue = getComputed(() => formatter.get(cell)); + + const nullValue = isResultSetContentValue(rawValue) ? rawValue.text === 'null' : rawValue === null; + + if (nullValue) { + return ; + } + + return ; +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.m.css deleted file mode 100644 index af9904cde0..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.m.css +++ /dev/null @@ -1,26 +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. - */ - -.booleanFormatter { - cursor: pointer; -} - -.disabled { - cursor: auto; -} - -.nullValue { - composes: nullValue from './CellNullValue.m.css'; -} - -.booleanFormatter:not(.nullValue) { - font-family: monospace; - white-space: pre; - line-height: 1; - vertical-align: text-top; -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.module.css new file mode 100644 index 0000000000..10dbe7b88c --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.module.css @@ -0,0 +1,8 @@ +@layer components { + .formatter { + composes: checkbox-markup-theme--tertiary from global; + --dbv-kit-checkbox-border-width: 1px; + --dbv-kit-checkbox-check-size: 0.8; + --dbv-kit-checkbox-small-height: 1.375rem; + } +} 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..3b77efadf9 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 @@ -1,64 +1,64 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useContext, useMemo } from 'react'; +import { useContext } from 'react'; -import { s, useS } from '@cloudbeaver/core-blocks'; -import type { IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer'; -import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid'; +import { BooleanFormatter as GridBooleanFormatter } from '@cloudbeaver/plugin-data-grid'; +import { getComputed, s, useS } from '@cloudbeaver/core-blocks'; +import { DatabaseEditChangeType } from '@cloudbeaver/plugin-data-viewer'; -import { EditingContext } from '../../../Editing/EditingContext'; -import { CellContext } from '../../CellRenderer/CellContext'; -import { DataGridContext } from '../../DataGridContext'; -import { TableDataContext } from '../../TableDataContext'; -import style from './BooleanFormatter.m.css'; +import { CellContext } from '../../CellRenderer/CellContext.js'; +import { DataGridContext } from '../../DataGridContext.js'; +import { TableDataContext } from '../../TableDataContext.js'; +import type { ICellFormatterProps } from '../ICellFormatterProps.js'; +import styles from './BooleanFormatter.module.css'; -export const BooleanFormatter = observer>(function BooleanFormatter({ column, row }) { +export const BooleanFormatter = observer(function BooleanFormatter() { const context = useContext(DataGridContext); const tableDataContext = useContext(TableDataContext); - const editingContext = useContext(EditingContext); const cellContext = useContext(CellContext); + const style = useS(styles); - if (!context || !tableDataContext || !editingContext || !cellContext.cell) { - throw new Error('Contexts required'); - } + const cell = cellContext.cell; - const styles = useS(style); + if (!context || !tableDataContext || !cell) { + return null; + } 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 disabled = + context.model.isReadonly(context.resultIndex) || (formatter.isReadOnly(cell) && cellContext.editionState !== DatabaseEditChangeType.add); 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 ( - - {valueRepresentation} - + ); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/CellNullValue.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/CellNullValue.m.css deleted file mode 100644 index 6a8f0c528c..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/CellNullValue.m.css +++ /dev/null @@ -1,12 +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. - */ - -.nullValue { - text-transform: uppercase; - opacity: 0.65; -} 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 deleted file mode 100644 index 9bc33bd293..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.m.css +++ /dev/null @@ -1,36 +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. - */ - -.textFormatter { - display: flex; - align-items: center; - - & .a { - display: flex; - align-items: center; - justify-content: center; - min-width: 16px; - height: 24px; - margin-right: 8px; - - & .icon { - width: 12px; - height: 12px; - } - } -} - -.textFormatterValue { - 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/TextFormatter.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.module.css new file mode 100644 index 0000000000..7ff141a740 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.module.css @@ -0,0 +1,36 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.textFormatter { + display: flex; + align-items: center; + overflow: hidden; + + & .a { + display: flex; + align-items: center; + justify-content: center; + min-width: 16px; + height: 24px; + margin-right: 8px; + + & .icon { + width: 12px; + height: 12px; + } + } + + & .loader { + height: 24px; + } +} + +.textFormatterValue { + overflow: hidden; + 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..11c9f10976 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 @@ -1,73 +1,50 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useCallback, useContext, useEffect, useRef } from 'react'; +import { useContext } from 'react'; import { getComputed, IconOrImage, s, useS } from '@cloudbeaver/core-blocks'; import { isValidUrl } from '@cloudbeaver/core-utils'; -import type { IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer'; -import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid'; +import { NullFormatter as GridNullFormatter } from '@cloudbeaver/plugin-data-grid'; -import { EditingContext } from '../../../Editing/EditingContext'; -import { CellEditor, IEditorRef } from '../../CellEditor'; -import { CellContext } from '../../CellRenderer/CellContext'; -import { TableDataContext } from '../../TableDataContext'; -import styles from './TextFormatter.m.css'; +import { CellContext } from '../../CellRenderer/CellContext.js'; +import { TableDataContext } from '../../TableDataContext.js'; +import styles from './TextFormatter.module.css'; +import type { ICellFormatterProps } from '../ICellFormatterProps.js'; -export const TextFormatter = observer>(function TextFormatter({ row, column }) { - const editorRef = useRef(null); - const editingContext = useContext(EditingContext); +export const TextFormatter = observer(function TextFormatter() { const tableDataContext = useContext(TableDataContext); const cellContext = useContext(CellContext); + const style = useS(styles); if (!cellContext.cell) { - throw new Error('Contexts required'); + return null; } - const style = useS(styles); const formatter = tableDataContext.format; - const rawValue = getComputed(() => formatter.get(tableDataContext.getCellValue(cellContext.cell!)!)); - - const classes = s(style, { textFormatter: true, nullValue: rawValue === null }); - - const value = formatter.toDisplayString(rawValue); - - const handleClose = useCallback(() => { - editingContext.closeEditor(cellContext.position); - }, [cellContext]); - - const isFocused = cellContext.isFocused; - useEffect(() => { - if (isFocused) { - if (cellContext.isEditing) { - editorRef.current?.focus(); - } - } - }, [isFocused]); + const nullValue = getComputed(() => formatter.get(cellContext.cell!) === null); + const textValue = getComputed(() => formatter.getText(cellContext.cell!)); + const displayValue = getComputed(() => formatter.getDisplayString(cellContext.cell!)); - if (cellContext.isEditing) { - return ( -
- -
- ); + if (nullValue) { + return ; } - const isUrl = typeof rawValue === 'string' && isValidUrl(rawValue); + const classes = s(style, { textFormatter: true }); return ( -
- {isUrl && ( - +
+ {isValidUrl(textValue) && ( + )} -
{value}
+
{displayValue}
); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/ICellFormatterProps.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/ICellFormatterProps.ts new file mode 100644 index 0000000000..6258826608 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/ICellFormatterProps.ts @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 ICellFormatterProps { + rowIdx: number; + colIdx: number; +} 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..59f6437eca 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 @@ -1,19 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { useContext } from 'react'; +import { observer } from 'mobx-react-lite'; -import type { IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer'; -import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid'; +import type { ICellFormatterProps } from './ICellFormatterProps.js'; -import { CellContext } from '../CellRenderer/CellContext'; - -export const IndexFormatter: React.FC> = function IndexFormatter(props) { - const context = useContext(CellContext); - - return
{context.position.rowIdx + 1}
; -}; +export const IndexFormatter: React.FC = observer(function IndexFormatter({ rowIdx }) { + return rowIdx + 1; +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.module.css new file mode 100644 index 0000000000..24dbef1750 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.module.css @@ -0,0 +1,38 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.container { + display: flex; +} + +.contextMenu { + padding: 0; + height: 16px; + width: 16px; + + &:before { + display: none; + } +} + +.trigger { + cursor: pointer; + display: flex; + align-items: center; + padding: 0 6px; + margin-right: -6px; +} + +.icon { + width: 16px; + height: 10px; +} + +.iconOrImage { + composes: theme-text-primary from global; +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx index 727f319e79..733ab9ce43 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx @@ -1,20 +1,32 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled from 'reshadow'; -import { Icon, MenuTrigger } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; +import { Icon, MenuItemElementStyles, s, SContext, type StyleRegistry, useS } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; -import type { IDatabaseDataModel, IDataPresentationActions, IDataTableActions, IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer'; +import { ContextMenu } from '@cloudbeaver/core-ui'; +import { useMenu } from '@cloudbeaver/core-view'; +import { + DATA_CONTEXT_DV_ACTIONS, + DATA_CONTEXT_DV_DDM, + DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_PRESENTATION_ACTIONS, + DATA_CONTEXT_DV_RESULT_KEY, + DATA_CONTEXT_DV_SIMPLE, + type IDatabaseDataModel, + type IDataPresentationActions, + type IDataTableActions, + type IResultSetElementKey, + MENU_DV_CONTEXT_MENU, +} from '@cloudbeaver/plugin-data-viewer'; -import { DataGridContextMenuService } from '../../DataGridContextMenu/DataGridContextMenuService'; -import { cellMenuStyles } from './cellMenuStyles'; +import classes from './CellMenu.module.css'; interface Props { model: IDatabaseDataModel; @@ -23,32 +35,31 @@ interface Props { resultIndex: number; cellKey: IResultSetElementKey; simple: boolean; - onClick?: () => void; onStateSwitch?: (state: boolean) => void; } -export const CellMenu = observer(function CellMenu({ - model, - actions, - spreadsheetActions, - resultIndex, - cellKey, - simple, - onClick, - onStateSwitch, -}) { - const dataGridContextMenuService = useService(DataGridContextMenuService); +const registry: StyleRegistry = [ + [ + MenuItemElementStyles, + { + mode: 'append', + styles: [classes], + }, + ], +]; - const panel = dataGridContextMenuService.constructMenuWithContext(model, actions, spreadsheetActions, resultIndex, cellKey, simple); +export const CellMenu = observer(function CellMenu({ model, actions, spreadsheetActions, resultIndex, cellKey, simple, onStateSwitch }) { + const style = useS(classes); + const menu = useMenu({ menu: MENU_DV_CONTEXT_MENU }); - if (!panel.menuItems.length || panel.menuItems.every(item => item.isHidden)) { - return null; - } - - function handleClick() { - dataGridContextMenuService.openMenu(model, actions, spreadsheetActions, resultIndex, cellKey, simple); - onClick?.(); - } + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_DV_DDM, model, id); + context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex, id); + context.set(DATA_CONTEXT_DV_SIMPLE, simple, id); + context.set(DATA_CONTEXT_DV_ACTIONS, actions, id); + context.set(DATA_CONTEXT_DV_PRESENTATION_ACTIONS, spreadsheetActions, id); + context.set(DATA_CONTEXT_DV_RESULT_KEY, cellKey, id); + }); function stopPropagation(event: React.MouseEvent) { event.stopPropagation(); @@ -58,11 +69,23 @@ export const CellMenu = observer(function CellMenu({ EventContext.set(event, EventStopPropagationFlag); } - return styled(cellMenuStyles)( - - - - - , + return ( + +
+ +
+ +
+
+
+
); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/cellMenuStyles.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/cellMenuStyles.ts deleted file mode 100644 index 02fa91eb7b..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/cellMenuStyles.ts +++ /dev/null @@ -1,38 +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 cellMenuStyles = css` - IconOrImage { - composes: theme-text-primary from global; - } - :global(.rdg-cell):not(:global([aria-selected='true'])):not(:hover) cell-menu { - display: none; - } - cell-menu { - flex: 0 0 auto; - height: var(--rdg-row-height); - position: absolute; - top: 0px; - right: 0px; - } - MenuTrigger { - padding: 0 8px; - height: 100%; - - &:before { - display: none; - } - - & Icon { - cursor: pointer; - width: 16px; - height: 10px; - } - } -`; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.m.css deleted file mode 100644 index 0ca86c00ee..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.m.css +++ /dev/null @@ -1,23 +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. - */ - -.orderButton { - display: flex; - flex-direction: column; - justify-content: center; - flex-shrink: 0; - height: 28px; - width: 16px; - box-sizing: border-box; - cursor: pointer; - background: transparent; - outline: none; - color: inherit; - padding: 0; - margin-left: 8px; -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx deleted file mode 100644 index e6d4bab82f..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx +++ /dev/null @@ -1,59 +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 { observer } from 'mobx-react-lite'; - -import { IconOrImage, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { EOrder, getNextOrder, IDatabaseDataModel, ResultSetConstraintAction } from '@cloudbeaver/plugin-data-viewer'; - -import style from './OrderButton.m.css'; - -interface Props { - model: IDatabaseDataModel; - resultIndex: number; - attributePosition: number; - className?: string; -} - -export const OrderButton = observer(function OrderButton({ model, resultIndex, attributePosition, className }) { - const translate = useTranslate(); - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - const currentOrder = constraints.getOrder(attributePosition); - const disabled = model.isDisabled(resultIndex) || model.isLoading(); - const styles = useS(style); - - let icon = 'order-arrow-unknown'; - if (currentOrder === EOrder.asc) { - icon = 'order-arrow-asc'; - } else if (currentOrder === EOrder.desc) { - icon = 'order-arrow-desc'; - } - - const handleSort = async (e: React.MouseEvent) => { - const nextOrder = getNextOrder(currentOrder); - await model.requestDataAction(async () => { - constraints.setOrder(attributePosition, nextOrder, e.ctrlKey || e.metaKey); - await model.request(true); - }); - }; - - function preventFocus(event: React.MouseEvent) { - event.preventDefault(); - } - - return ( - - ); -}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.m.css deleted file mode 100644 index cb30b7e623..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.m.css +++ /dev/null @@ -1,59 +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. - */ - -.header { - display: flex; - align-items: center; - align-content: center; - width: 100%; -} -.container { - display: flex; - align-items: center; - flex: 1 1 auto; - overflow: hidden; - cursor: pointer; -} -.icon { - display: flex; - position: relative; -} -.staticImage { - height: 16px; -} -.name { - margin-left: 8px; - font-weight: 400; - flex-grow: 1; -} -.readonlyStatus { - position: absolute; - bottom: 0; - right: 0; - width: 8px; - height: 8px; - border-radius: 50%; - border: 1px solid; -} -.dragging { - opacity: 0.5; -} -.header:before { - position: absolute; - z-index: 10; - height: 100%; - border-left: solid 2px var(--theme-primary); -} -[data-s-rearrange='left']:before { - content: ''; - left: 0; -} -[data-s-rearrange='right']:before { - content: ''; - right: 0; -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.module.css new file mode 100644 index 0000000000..ed1921e4eb --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.module.css @@ -0,0 +1,67 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.header { + display: flex; + align-content: center; + width: 100%; + gap: 4px; + cursor: pointer; +} +.icon { + display: flex; + position: relative; + flex-shrink: 0; + width: 16px; + height: 16px; + justify-content: center; + align-items: center; +} +.staticImage { + height: 16px; + display: block; +} +.name { + font-weight: 400; + flex-grow: 1; +} +.readonlyStatus { + position: absolute; + bottom: 0; + right: 0; + width: 8px; + height: 8px; + border-radius: 50%; + border: 1px solid; + + &.independent { + top: 2px; + left: 2px; + } +} +.dragging { + opacity: 0.5; +} +.header:before { + position: absolute; + z-index: 10; + height: 100%; + border-left: solid 2px var(--theme-primary); +} +[data-s-rearrange='left']:before { + content: ''; + left: 0; +} +[data-s-rearrange='right']:before { + content: ''; + right: 0; +} +.description { + composes: theme-typography--caption from global; + color: var(--theme-on-secondary); +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx index 49457451d2..5c7625b5bc 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,17 +9,19 @@ import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; import { getComputed, s, StaticImage, useS } from '@cloudbeaver/core-blocks'; -import type { SqlResultColumn } from '@cloudbeaver/core-sdk'; -import type { RenderHeaderCellProps } from '@cloudbeaver/plugin-react-data-grid'; -import { DataGridContext } from '../DataGridContext'; -import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext'; -import { TableDataContext } from '../TableDataContext'; -import { OrderButton } from './OrderButton'; -import style from './TableColumnHeader.m.css'; -import { useTableColumnDnD } from './useTableColumnDnD'; +import { DataGridContext } from '../DataGridContext.js'; +import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext.js'; +import { TableDataContext } from '../TableDataContext.js'; +import style from './TableColumnHeader.module.css'; +import { useTableColumnDnD } from './useTableColumnDnD.js'; +import { HEADER_HEIGHT } from '../DataGridTable.js'; -export const TableColumnHeader = observer>(function TableColumnHeader({ column: calculatedColumn }) { +interface Props { + colIdx: number; +} + +export const TableColumnHeader = observer(function TableColumnHeader({ colIdx }) { const dataGridContext = useContext(DataGridContext); const tableDataContext = useContext(TableDataContext); const gridSelectionContext = useContext(DataGridSelectionContext); @@ -28,25 +30,26 @@ export const TableColumnHeader = observer>(function T const resultIndex = dataGridContext.resultIndex; const model = dataGridContext.model; - const dnd = useTableColumnDnD(model, resultIndex, calculatedColumn.columnDataIndex); + const columnInfo = tableDataContext.getColumn(colIdx)!; + const dnd = useTableColumnDnD(model, resultIndex, columnInfo.key); - const dataReadonly = getComputed(() => tableDataContext.isReadOnly() || model.isReadonly(resultIndex)); - const sortingDisabled = getComputed(() => !tableDataContext.constraints.supported || !model.source.executionContext?.context); + const dataReadonly = getComputed(() => model.isReadonly(resultIndex)); + const hasElementIdentifier = getComputed(() => model.hasElementIdentifier(resultIndex)); - let resultColumn: SqlResultColumn | undefined; - let icon = calculatedColumn.icon; - let columnName = calculatedColumn.name as string; - let columnReadOnly = !calculatedColumn.editable; - let columnTooltip: string = columnName; + let icon: string | undefined; + let columnName: string | undefined; + let columnReadOnly = false; + let columnTooltip: string | undefined; + let columnDescription: string | undefined; - if (calculatedColumn.columnDataIndex !== null) { - const column = tableDataContext.data.getColumn(calculatedColumn.columnDataIndex); + if (columnInfo.key !== null) { + const column = tableDataContext.data.getColumn(columnInfo.key); if (column) { - resultColumn = column; columnName = column.label!; + columnDescription = column.description; icon = column.icon; - columnReadOnly ||= tableDataContext.format.isReadOnly({ column: calculatedColumn.columnDataIndex }); + columnReadOnly ||= tableDataContext.format.isReadOnly({ column: columnInfo.key }); columnTooltip = columnName; @@ -61,20 +64,47 @@ export const TableColumnHeader = observer>(function T } function handleClick(event: React.MouseEvent) { - gridSelectionContext.selectColumn(calculatedColumn.idx, event.ctrlKey || event.metaKey); + gridSelectionContext.selectColumn(colIdx, event.ctrlKey || event.metaKey); dataGridContext.focus(); } return ( -
-
-
- {icon && } - {!dataReadonly && columnReadOnly &&
} +
+
+ {dataReadonly && colIdx === 0 && ( +
+ )} +
+
+ {icon && ( +
+ + {columnReadOnly && hasElementIdentifier && !dataReadonly && ( +
+ )} +
+ )} +
{columnName}
+
+ {tableDataContext.hasDescription && columnDescription && ( +
+ {columnDescription} +
+ )}
-
{columnName}
- {!sortingDisabled && resultColumn && }
); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.m.css deleted file mode 100644 index 6fc0c0c5dd..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.m.css +++ /dev/null @@ -1,20 +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. - */ - -.container { - width: 100%; - cursor: pointer; -} -.iconOrImage { - cursor: auto; - width: 10px; - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.module.css new file mode 100644 index 0000000000..c60f08c603 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.module.css @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.clickAreaOverlay { + cursor: pointer; + position: absolute; + + top: 0; + left: 0; + bottom: 0; + right: 0; +} + +.iconOrImage { + cursor: auto; + width: 10px; + position: absolute; + top: 50%; + transform: translateY(-50%); +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx index 9351c5f06d..60574fcb8f 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,14 +9,13 @@ import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; import { getComputed, IconOrImage, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import type { RenderHeaderCellProps } from '@cloudbeaver/plugin-react-data-grid'; -import { DataGridContext } from '../DataGridContext'; -import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext'; -import { TableDataContext } from '../TableDataContext'; -import style from './TableIndexColumnHeader.m.css'; +import { DataGridContext } from '../DataGridContext.js'; +import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext.js'; +import { TableDataContext } from '../TableDataContext.js'; +import style from './TableIndexColumnHeader.module.css'; -export const TableIndexColumnHeader = observer>(function TableIndexColumnHeader(props) { +export const TableIndexColumnHeader = observer(function TableIndexColumnHeader() { const dataGridContext = useContext(DataGridContext); const selectionContext = useContext(DataGridSelectionContext); const tableDataContext = useContext(TableDataContext); @@ -27,7 +26,9 @@ export const TableIndexColumnHeader = observer>(funct throw new Error('Contexts required'); } - const readonly = getComputed(() => tableDataContext.isReadOnly() || dataGridContext.model.isReadonly(dataGridContext.resultIndex)); + const readonly = getComputed( + () => dataGridContext.model.isReadonly(dataGridContext.resultIndex) || !dataGridContext.model.hasElementIdentifier(dataGridContext.resultIndex), + ); function handleClick(event: React.MouseEvent) { selectionContext.selectTable(); @@ -35,11 +36,16 @@ export const TableIndexColumnHeader = observer>(funct } return ( -
+ <> {readonly && ( )} - {props.column.name} -
+
+ ); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/useTableColumnDnD.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/useTableColumnDnD.ts index ab24da00f7..b24d833ce2 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/useTableColumnDnD.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/useTableColumnDnD.ts @@ -1,19 +1,21 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { useCombinedRef } from '@cloudbeaver/core-blocks'; -import { useDataContext } from '@cloudbeaver/core-data-context'; -import { IDNDBox, IDNDData, useDNDBox, useDNDData } from '@cloudbeaver/core-ui'; +import { useDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; +import { type IDNDBox, type IDNDData, useDNDBox, useDNDData } from '@cloudbeaver/core-ui'; import { DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY, - IDatabaseDataModel, - IResultSetColumnKey, + type IDatabaseDataModel, + type IResultSetColumnKey, + isResultSetDataModel, + ResultSetDataSource, ResultSetViewAction, } from '@cloudbeaver/plugin-data-viewer'; @@ -28,11 +30,17 @@ interface TableColumnDnD { export function useTableColumnDnD(model: IDatabaseDataModel, resultIndex: number, columnKey: IResultSetColumnKey | null): TableColumnDnD { const context = useDataContext(); - const resultSetViewAction = model.source.tryGetAction(resultIndex, ResultSetViewAction); + let resultSetViewAction: ResultSetViewAction | undefined; - context.set(DATA_CONTEXT_DV_DDM, model); - context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex); - context.set(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY, columnKey); + if (isResultSetDataModel(model)) { + resultSetViewAction = (model.source as ResultSetDataSource).tryGetAction(resultIndex, ResultSetViewAction); + } + + useDataContextLink(context, (context, id) => { + context.set(DATA_CONTEXT_DV_DDM, model, id); + context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex, id); + context.set(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY, columnKey, id); + }); const dndData = useDNDData(context, { canDrag: () => !model.isDisabled(resultIndex), 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..be1e877dad 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -14,54 +14,42 @@ import type { IResultSetElementKey, IResultSetRowKey, IResultSetValue, - ResultSetConstraintAction, ResultSetDataAction, + ResultSetDataContentAction, ResultSetEditAction, ResultSetFormatAction, ResultSetViewAction, } from '@cloudbeaver/plugin-data-viewer'; -import type { Column } from '@cloudbeaver/plugin-react-data-grid'; -declare module 'react-data-grid' { - interface Column { - columnDataIndex: IResultSetColumnKey | null; - icon?: string; - } -} - -interface IColumnMetrics { - width: number; - left: number; - right: number; +export interface IColumnInfo { + key: IResultSetColumnKey | null; } export interface ITableData { format: ResultSetFormatAction; + dataContent: ResultSetDataContentAction; data: ResultSetDataAction; editor: ResultSetEditAction; + hasDescription: boolean; view: ResultSetViewAction; - constraints: ResultSetConstraintAction; - columns: Array>; + columns: Array; columnKeys: IResultSetColumnKey[]; rows: IResultSetRowKey[]; gridDiv: HTMLDivElement | null; inBounds: (position: IResultSetElementKey) => boolean; - getMetrics: (columnIndex: number) => IColumnMetrics | undefined; getRow: (rowIndex: number) => IResultSetRowKey | undefined; - getColumn: (columnIndex: number) => Column | undefined; - getColumnByDataIndex: (key: IResultSetColumnKey) => Column; + getColumn: (columnIndex: number) => IColumnInfo | undefined; + getColumnByDataIndex: (key: IResultSetColumnKey) => IColumnInfo; getCellValue: (key: IResultSetElementKey) => IResultSetValue | undefined; getColumnInfo: (key: IResultSetColumnKey) => SqlResultColumn | undefined; - getColumnsInRange: (startIndex: number, endIndex: number) => Array>; - getColumnIndexFromKey: (columnKey: string) => number; + getColumnsInRange: (startIndex: number, endIndex: number) => Array; getColumnIndexFromColumnKey: (column: IResultSetColumnKey) => number; getRowIndexFromKey: (row: IResultSetRowKey) => number; getEditionState: (key: IResultSetElementKey) => DatabaseEditChangeType | null; isCellEdited: (key: IResultSetElementKey) => boolean; - isIndexColumn: (columnKey: string) => boolean; - isIndexColumnInRange: (columnsRange: Array>) => boolean; - isReadOnly: () => boolean; - isCellReadonly: (key: Partial) => boolean; + isIndexColumn: (columnKey: IColumnInfo) => boolean; + isIndexColumnInRange: (columnsRange: Array) => boolean; + isCellReadonly: (key: IResultSetElementKey) => boolean; } export const TableDataContext = createContext(undefined as any); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts index 704b669d7e..9c4039c296 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. 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..f5d94a00d8 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,26 +8,33 @@ import { useCallback } from 'react'; import { useObjectRef } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; import { copyToClipboard } from '@cloudbeaver/core-utils'; -import { IResultSetColumnKey, IResultSetElementKey, ResultSetDataKeysUtils, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer'; - -import type { IDataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext'; -import type { ITableData } from './TableDataContext'; +import { + DatabaseSelectAction, + DataViewerService, + type IResultSetColumnKey, + type IResultSetElementKey, + ResultSetDataKeysUtils, + ResultSetSelectAction, + useDataViewerCopyHandler, +} from '@cloudbeaver/plugin-data-viewer'; + +import type { IDataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext.js'; +import type { ITableData } from './TableDataContext.js'; const EVENT_KEY_CODE = { C: 'KeyC', }; 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) { const orderedSelectedCells = new Map( - [...selectedCells].sort((a, b) => tableData.getRowIndexFromKey(a[1][0].row) - tableData.getRowIndexFromKey(b[1][0].row)), + [...selectedCells].sort((a, b) => tableData.getRowIndexFromKey(a[1]![0]!.row) - tableData.getRowIndexFromKey(b[1]![0]!.row)), ); const selectedColumns: IResultSetColumnKey[] = []; @@ -61,27 +68,45 @@ function getSelectedCellsValue(tableData: ITableData, selectedCells: Map { if ((event.ctrlKey || event.metaKey) && event.nativeEvent.code === EVENT_KEY_CODE.C) { + const activeElement = document.activeElement as HTMLElement | null; + if ( + activeElement?.getAttribute('role') !== 'gridcell' && + activeElement?.getAttribute('role') !== 'columnheader' && + event.target !== event.currentTarget + ) { + return; + } EventContext.set(event, EventStopPropagationFlag); - const focusedElement = props.resultSetSelectAction.getFocusedElement(); - let value: string | null = null; + if (dataViewerService.canCopyData) { + if (!(props.selectAction instanceof ResultSetSelectAction)) { + throw new Error('Copying data is not supported'); + } - if (Array.from(props.selectionContext.selectedCells.keys()).length > 0) { - value = getSelectedCellsValue(props.tableData, props.selectionContext.selectedCells); - } else if (focusedElement) { - value = getCellCopyValue(tableData, focusedElement); - } + const focusedElement = props.selectAction?.getFocusedElement(); + let value: string | null = null; - if (value !== null) { - copyToClipboard(value); + if (Array.from(props.selectionContext.selectedCells.keys()).length > 0) { + value = getSelectedCellsValue(props.tableData, props.selectionContext.selectedCells); + } else if (focusedElement) { + value = getCellCopyValue(tableData, focusedElement); + } + + if (value !== null) { + copyToClipboard(value); + } } + + copyEventHandler(event); } }, []); 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..d112d4164c 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,49 +8,32 @@ import { computed, observable } from 'mobx'; import { useObservableRef } from '@cloudbeaver/core-blocks'; -import { TextTools } from '@cloudbeaver/core-utils'; import { - IDatabaseDataModel, - IDatabaseResultSet, - IResultSetColumnKey, - IResultSetElementKey, - IResultSetRowKey, - ResultSetConstraintAction, + DatabaseEditChangeType, + type IDatabaseDataModel, + type IResultSetColumnKey, + type IResultSetElementKey, + type IResultSetRowKey, ResultSetDataAction, + ResultSetDataContentAction, ResultSetDataKeysUtils, + ResultSetDataSource, ResultSetEditAction, ResultSetFormatAction, ResultSetViewAction, } from '@cloudbeaver/plugin-data-viewer'; -import type { Column } from '@cloudbeaver/plugin-react-data-grid'; -import { IndexFormatter } from './Formatters/IndexFormatter'; -import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader'; -import { TableIndexColumnHeader } from './TableColumnHeader/TableIndexColumnHeader'; -import type { ITableData } from './TableDataContext'; +import type { IColumnInfo, ITableData } from './TableDataContext.js'; +import { useService } from '@cloudbeaver/core-di'; +import { DataGridSettingsService } from '../DataGridSettingsService.js'; -export const indexColumn: Column = { - key: 'index', - columnDataIndex: null, - name: '#', - minWidth: 60, - width: 60, - resizable: false, - frozen: true, - renderHeaderCell: props => , - renderCell: props => , -}; - -const COLUMN_PADDING = 16 + 2; -const COLUMN_HEADER_ICON_WIDTH = 16; -const COLUMN_HEADER_TEXT_PADDING = 8; -const COLUMN_HEADER_ORDER_PADDING = 8; -const COLUMN_HEADER_ORDER_WIDTH = 16; - -const FONT = '400 12px Roboto'; +interface ITableDataPrivate extends ITableData { + dataGridSettingsService: DataGridSettingsService; + gridDIVElement: React.RefObject; +} export function useTableData( - model: IDatabaseDataModel, + model: IDatabaseDataModel, resultIndex: number, gridDIVElement: React.RefObject, ): ITableData { @@ -58,9 +41,10 @@ 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 constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const dataContent = model.source.getAction(resultIndex, ResultSetDataContentAction); + const dataGridSettingsService = useService(DataGridSettingsService); - return useObservableRef }>( + return useObservableRef( () => ({ get gridDiv(): HTMLDivElement | null { return this.gridDIVElement.current; @@ -75,52 +59,20 @@ export function useTableData( if (this.columnKeys.length === 0) { return []; } - const columnNames = this.format.getHeaders(); - const rowStrings = this.format.getLongestCells(); - - const columnsWidth = TextTools.getWidth({ - font: FONT, - text: columnNames, - }).map( - width => - width + COLUMN_PADDING + COLUMN_HEADER_ICON_WIDTH + COLUMN_HEADER_TEXT_PADDING + COLUMN_HEADER_ORDER_PADDING + COLUMN_HEADER_ORDER_WIDTH, - ); - - const cellsWidth = TextTools.getWidth({ - font: FONT, - text: rowStrings, - }).map(width => width + COLUMN_PADDING); - const columns: Array> = this.columnKeys.map>((col, index) => ({ - key: ResultSetDataKeysUtils.serialize(col), - columnDataIndex: col, - name: this.getColumnInfo(col)?.label || '?', - editable: true, - width: Math.min(300, Math.max(columnsWidth[index], cellsWidth[index] ?? 0)), - renderHeaderCell: props => , + const columns: Array = this.columnKeys.map(col => ({ + key: col, })); - columns.unshift(indexColumn); + columns.unshift({ key: null }); return columns; }, - getMetrics(columnIndex) { - if (columnIndex < 0 || columnIndex > this.columns.length) { - return undefined; + get hasDescription(): boolean { + if (!this.dataGridSettingsService.description) { + return false; } - let left = 0; - for (let i = 0; i < columnIndex; i++) { - const column = this.columns[i]; - left += column.width as number; - } - - const column = this.getColumn(columnIndex)!; - - return { - left, - right: left + (column.width as number), - width: column.width as number, - }; + return Boolean(this.data?.columns?.some(column => column.description)); }, getRow(rowIndex) { return this.rows[rowIndex]; @@ -129,7 +81,7 @@ export function useTableData( return this.columns[columnIndex]; }, getColumnByDataIndex(key) { - return this.columns.find(column => column.columnDataIndex !== null && ResultSetDataKeysUtils.isEqual(column.columnDataIndex, key))!; + return this.columns.find(column => column.key !== null && ResultSetDataKeysUtils.isEqual(column.key, key))!; }, getColumnInfo(key) { return this.data.getColumn(key); @@ -137,18 +89,15 @@ export function useTableData( getCellValue(key) { return this.view.getCellValue(key); }, - getColumnIndexFromKey(key) { - return this.columns.findIndex(column => column.key === key); - }, getColumnIndexFromColumnKey(columnKey) { - return this.columns.findIndex(column => column.columnDataIndex !== null && ResultSetDataKeysUtils.isEqual(columnKey, column.columnDataIndex)); + return this.columns.findIndex(column => column.key !== null && ResultSetDataKeysUtils.isEqual(columnKey, column.key)); }, getRowIndexFromKey(rowKey) { return this.rows.findIndex(row => ResultSetDataKeysUtils.isEqual(rowKey, row)); }, - getColumnsInRange(startIndex, endIndex) { + getColumnsInRange(startIndex, endIndex): IColumnInfo[] { if (startIndex === endIndex) { - return [this.columns[startIndex]]; + return [this.columns[startIndex]!]; } const firstIndex = Math.min(startIndex, endIndex); @@ -165,42 +114,42 @@ export function useTableData( return this.editor.isElementEdited(key); }, isIndexColumn(columnKey) { - return columnKey === indexColumn.key; + return columnKey.key === null; }, isIndexColumnInRange(columnsRange) { - return columnsRange.some(column => this.isIndexColumn(column.key)); + return columnsRange.some(column => this.isIndexColumn(column)); }, isReadOnly() { - return this.columnKeys.every(column => this.getColumnInfo(column)?.readOnly); + return dataContent.source.isReadonly(resultIndex); }, - isCellReadonly(key: Partial) { + isCellReadonly(key: IResultSetElementKey) { if (!key.column) { return true; } - const column = this.getColumnByDataIndex(key.column); - - return !column.editable || this.format.isReadOnly(key); + return model.isReadonly(resultIndex) || (this.format.isReadOnly(key) && this.editor.getElementState(key) !== DatabaseEditChangeType.add); }, }), { columns: computed, rows: computed, columnKeys: computed, + hasDescription: computed, format: observable.ref, + dataContent: observable.ref, data: observable.ref, editor: observable.ref, view: observable.ref, - constraints: observable.ref, gridDIVElement: observable.ref, }, { format, + dataContent, data, editor, view, - constraints, gridDIVElement, + dataGridSettingsService, }, ); } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts index b1831f1c08..e275b205c4 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts @@ -1,124 +1,113 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 '@testing-library/jest-dom'; - -import { coreAdministrationManifest } from '@cloudbeaver/core-administration'; -import { coreAppManifest } from '@cloudbeaver/core-app'; -import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; -import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; -import { coreBrowserManifest } from '@cloudbeaver/core-browser'; -import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; -import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; -import { coreEventsManifest } from '@cloudbeaver/core-events'; -import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; -import { coreProjectsManifest } from '@cloudbeaver/core-projects'; -import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; -import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; -import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; -import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; -import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; -import { coreRoutingManifest } from '@cloudbeaver/core-routing'; -import { coreSDKManifest } from '@cloudbeaver/core-sdk'; -import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { coreThemingManifest } from '@cloudbeaver/core-theming'; -import { coreUIManifest } from '@cloudbeaver/core-ui'; -import { coreViewManifest } from '@cloudbeaver/core-view'; -import { dataViewerManifest } from '@cloudbeaver/plugin-data-viewer'; -import { datasourceContextSwitchPluginManifest } from '@cloudbeaver/plugin-datasource-context-switch'; -import { navigationTabsPlugin } from '@cloudbeaver/plugin-navigation-tabs'; -import { navigationTreePlugin } from '@cloudbeaver/plugin-navigation-tree'; -import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; -import { createApp } from '@cloudbeaver/tests-runner'; - -import { DataGridSettings, DataGridSettingsService } from './DataGridSettingsService'; -import { dataSpreadsheetNewManifest } from './manifest'; - -const endpoint = createGQLEndpoint(); -const app = createApp( - dataSpreadsheetNewManifest, - coreLocalizationManifest, - coreEventsManifest, - corePluginManifest, - coreProductManifest, - coreRootManifest, - coreSDKManifest, - coreBrowserManifest, - coreSettingsManifest, - coreViewManifest, - coreAuthenticationManifest, - coreProjectsManifest, - coreUIManifest, - coreRoutingManifest, - coreAdministrationManifest, - coreConnectionsManifest, - coreDialogsManifest, - coreNavigationTree, - coreAppManifest, - coreThemingManifest, - datasourceContextSwitchPluginManifest, - navigationTreePlugin, - navigationTabsPlugin, - objectViewerManifest, - dataViewerManifest, -); - -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); - -beforeAll(() => app.init()); - -const testValueA = true; -const testValueB = false; - -const equalConfigA = { - plugin_data_spreadsheet_new: { - hidden: testValueA, - } as DataGridSettings, - plugin: { - 'data-spreadsheet': { - hidden: testValueA, - } as DataGridSettings, - }, -}; - -const equalConfigB = { - plugin_data_spreadsheet_new: { - hidden: testValueB, - } as DataGridSettings, - plugin: { - 'data-spreadsheet': { - hidden: testValueB, - } as DataGridSettings, - }, -}; - -test('New settings equal deprecated settings A', async () => { - const settings = app.injector.getServiceByClass(DataGridSettingsService); - const config = app.injector.getServiceByClass(ServerConfigResource); - - server.use(endpoint.query('serverConfig', mockServerConfig(equalConfigA))); - - await config.refresh(); - - expect(settings.settings.getValue('hidden')).toBe(testValueA); - expect(settings.deprecatedSettings.getValue('hidden')).toBe(testValueA); -}); - -test('New settings equal deprecated settings B', async () => { - const settings = app.injector.getServiceByClass(DataGridSettingsService); - const config = app.injector.getServiceByClass(ServerConfigResource); - - server.use(endpoint.query('serverConfig', mockServerConfig(equalConfigB))); - - await config.refresh(); - - expect(settings.settings.getValue('hidden')).toBe(testValueB); - expect(settings.deprecatedSettings.getValue('hidden')).toBe(testValueB); -}); +import { describe } from 'vitest'; + +// import { coreAdministrationManifest } from '@cloudbeaver/core-administration'; +// import { coreAppManifest } from '@cloudbeaver/core-app'; +// import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; +// import { mockAuthentication } from '@cloudbeaver/core-authentication/__custom_mocks__/mockAuthentication.js'; +// import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +// import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; +// import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; +// import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; +// import { coreEventsManifest } from '@cloudbeaver/core-events'; +// import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; +// import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; +// import { coreProjectsManifest } from '@cloudbeaver/core-projects'; +// import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; +// import { createGQLEndpoint } from '@cloudbeaver/core-root/__custom_mocks__/createGQLEndpoint.js'; +// import '@cloudbeaver/core-root/__custom_mocks__/expectWebsocketClosedMessage.js'; +// import { mockAppInit } from '@cloudbeaver/core-root/__custom_mocks__/mockAppInit.js'; +// import { mockGraphQL } from '@cloudbeaver/core-root/__custom_mocks__/mockGraphQL.js'; +// import { mockServerConfig } from '@cloudbeaver/core-root/__custom_mocks__/resolvers/mockServerConfig.js'; +// import { coreRoutingManifest } from '@cloudbeaver/core-routing'; +// import { coreSDKManifest } from '@cloudbeaver/core-sdk'; +// import { coreSettingsManifest } from '@cloudbeaver/core-settings'; +// import { +// expectDeprecatedSettingMessage, +// expectNoDeprecatedSettingMessage, +// } from '@cloudbeaver/core-settings/__custom_mocks__/expectDeprecatedSettingMessage.js'; +// import { coreStorageManifest } from '@cloudbeaver/core-storage'; +// import { coreUIManifest } from '@cloudbeaver/core-ui'; +// import { coreViewManifest } from '@cloudbeaver/core-view'; +// import { dataViewerManifest } from '@cloudbeaver/plugin-data-viewer'; +// import { datasourceContextSwitchPluginManifest } from '@cloudbeaver/plugin-datasource-context-switch'; +// import { navigationTabsPlugin } from '@cloudbeaver/plugin-navigation-tabs'; +// import { navigationTreePlugin } from '@cloudbeaver/plugin-navigation-tree'; +// import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; +// import { createApp } from '@cloudbeaver/tests-runner'; + +// import { DataGridSettingsService } from './DataGridSettingsService.js'; +// import { dataSpreadsheetNewManifest } from './manifest.js'; + +// const endpoint = createGQLEndpoint(); +// const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); +// const app = createApp( +// dataSpreadsheetNewManifest, +// coreLocalizationManifest, +// coreEventsManifest, +// coreRootManifest, +// coreSDKManifest, +// coreBrowserManifest, +// coreSettingsManifest, +// coreViewManifest, +// coreStorageManifest, +// coreAuthenticationManifest, +// coreProjectsManifest, +// coreUIManifest, +// coreRoutingManifest, +// coreAdministrationManifest, +// coreConnectionsManifest, +// coreDialogsManifest, +// coreNavigationTree, +// coreAppManifest, +// datasourceContextSwitchPluginManifest, +// navigationTreePlugin, +// navigationTabsPlugin, +// objectViewerManifest, +// dataViewerManifest, +// coreClientActivityManifest, +// ); + +// const testValueDeprecated = true; +// const testValueNew = false; + +// const deprecatedSettings = { +// 'plugin_data_spreadsheet_new.hidden': testValueDeprecated, +// }; + +// const newSettings = { +// ...deprecatedSettings, +// 'plugin.data-spreadsheet.hidden': testValueNew, +// }; + +// test('New settings override deprecated', async () => { +// const settings = app.serviceProvider.getService(DataGridSettingsService); +// const config = app.serviceProvider.getService(ServerConfigResource); + +// server.use(endpoint.query('serverConfig', mockServerConfig(newSettings))); + +// await config.refresh(); + +// expect(settings.hidden).toBe(testValueNew); +// expectNoDeprecatedSettingMessage(); +// }); + +// test('Deprecated settings are used if new settings are not defined', async () => { +// const settings = app.serviceProvider.getService(DataGridSettingsService); +// const config = app.serviceProvider.getService(ServerConfigResource); + +// server.use(endpoint.query('serverConfig', mockServerConfig(deprecatedSettings))); + +// await config.refresh(); + +// expect(settings.hidden).toBe(testValueDeprecated); +// expectDeprecatedSettingMessage(); +// }); + +describe.skip('DataGridSettingsService', () => {}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts index 225a9eeb83..a9fa5351ef 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts @@ -1,27 +1,82 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; +import { + createSettingsAliasResolver, + ESettingsValueType, + ROOT_SETTINGS_LAYER, + SettingsManagerService, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; +import { DATA_EDITOR_SETTINGS_GROUP } from '@cloudbeaver/plugin-data-viewer'; -const defaultSettings = { - hidden: false, -}; +const defaultSettings = schema.object({ + 'plugin.data-spreadsheet.hidden': schemaExtra.stringedBoolean().default(false), + 'plugin.data-spreadsheet.showDescriptionInHeader': schemaExtra.stringedBoolean().default(true), +}); -export type DataGridSettings = typeof defaultSettings; +export type DataGridSettingsSchema = typeof defaultSettings; +export type DataGridSettings = schema.infer; -@injectable() +@injectable(() => [SettingsProviderService, SettingsManagerService, SettingsResolverService]) export class DataGridSettingsService { - readonly settings: PluginSettings; - /** @deprecated Use settings instead, will be removed in 23.0.0 */ - readonly deprecatedSettings: PluginSettings; + get hidden(): boolean { + return this.settings.getValue('plugin.data-spreadsheet.hidden'); + } + + get description(): boolean { + return this.settings.getValue('plugin.data-spreadsheet.showDescriptionInHeader'); + } + + readonly settings: SettingsProvider; + + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + private readonly settingsResolverService: SettingsResolverService, + ) { + this.settings = this.settingsProviderService.createSettings(defaultSettings); + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, + /** @deprecated Use settings instead, will be removed in 23.0.0 */ + createSettingsAliasResolver(this.settingsProviderService.settingsResolver, { + 'plugin.data-spreadsheet.hidden': 'plugin_data_spreadsheet_new.hidden', + }), + ); + + this.registerSettings(); + } - constructor(private readonly pluginManagerService: PluginManagerService) { - this.settings = this.pluginManagerService.createSettings('data-spreadsheet', 'plugin', defaultSettings); - this.deprecatedSettings = this.pluginManagerService.getDeprecatedPluginSettings('plugin_data_spreadsheet_new', defaultSettings); + private registerSettings() { + this.settingsManagerService.registerSettings(() => [ + { + group: DATA_EDITOR_SETTINGS_GROUP, + key: 'plugin.data-spreadsheet.hidden', + access: { + scope: ['role'], + }, + type: ESettingsValueType.Checkbox, + name: 'plugin_data_spreadsheet_new_settings_disable', + description: 'plugin_data_spreadsheet_new_settings_disable_description', + }, + { + group: DATA_EDITOR_SETTINGS_GROUP, + key: 'plugin.data-spreadsheet.showDescriptionInHeader', + access: { + scope: ['client'], + }, + type: ESettingsValueType.Checkbox, + name: 'plugin_data_spreadsheet_new_settings_description_label', + description: 'plugin_data_spreadsheet_new_settings_description_label_description', + }, + ]); } } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx deleted file mode 100644 index 658519794a..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx +++ /dev/null @@ -1,24 +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 { createContext } from 'react'; - -export interface IEditingContext { - readonly readonly: boolean; - edit: (position: CellPosition, code?: string, key?: string) => void; - closeEditor: (position: CellPosition) => void; - close: () => void; - isEditorActive: () => boolean; - isEditing: (position: CellPosition) => boolean; -} - -export interface CellPosition { - idx: number; - rowIdx: number; -} - -export const EditingContext = createContext(undefined as any); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/useEditing.ts b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/useEditing.ts deleted file mode 100644 index a7c2eb743c..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/useEditing.ts +++ /dev/null @@ -1,86 +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 { observable } from 'mobx'; -import { useState } from 'react'; - -import { useObservableRef } from '@cloudbeaver/core-blocks'; -import { MetadataMap } from '@cloudbeaver/core-utils'; - -import type { CellPosition, IEditingContext } from './EditingContext'; - -interface IEditingState { - editing: boolean; -} - -function getPositionHash(position: CellPosition): string { - return `${position.idx}_${position.rowIdx}`; -} - -interface IEditingOptions { - readonly?: boolean; - onEdit: (position: CellPosition, code?: string, key?: string) => boolean; - onCloseEditor?: (position: CellPosition) => void; -} - -export function useEditing(options: IEditingOptions): IEditingContext { - const state = useObservableRef( - () => ({ - editingCells: new MetadataMap(() => ({ editing: false })), - editorOpened: false, - }), - { - editorOpened: observable.ref, - readonly: observable.ref, - }, - { options, readonly: !!options.readonly }, - ); - - const [context] = useState({ - get readonly() { - return state.readonly; - }, - edit(position: CellPosition, code?: string, key?: string) { - if (state.options.readonly) { - return; - } - - if (!state.options.onEdit(position, code, key)) { - return; - } - - state.editingCells.clear(); - const info = state.editingCells.get(getPositionHash(position)); - info.editing = true; - state.editorOpened = true; - }, - closeEditor(position: CellPosition) { - const info = state.editingCells.get(getPositionHash(position)); - - if (!info.editing) { - return; - } - - info.editing = false; - state.editorOpened = false; - - state.options.onCloseEditor?.(position); - }, - close() { - state.editingCells.clear(); - state.editorOpened = false; - }, - isEditorActive() { - return state.editorOpened; - }, - isEditing(position: CellPosition) { - return state.editingCells.get(getPositionHash(position)).editing; - }, - }); - - return context; -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/LocaleService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/LocaleService.ts index e3649a06b4..cd5cdaf6f3 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/LocaleService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,32 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'de': + return (await import('./locales/de.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts index ffb1bab96d..50e4ec1a00 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts @@ -1,24 +1,48 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ExceptionsCatcherService } from '@cloudbeaver/core-events'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { DataPresentationService } from '@cloudbeaver/plugin-data-viewer'; +import { ACTION_DELETE, ACTION_OPEN, ActionService, MenuService } from '@cloudbeaver/core-view'; +import { + DATA_CONTEXT_DV_ACTIONS, + DATA_CONTEXT_DV_DDM, + DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_SIMPLE, + DatabaseDataConstraintAction, + DataPresentationService, + isResultSetDataSource, + MENU_DV_CONTEXT_MENU, + ResultSetDataSource, +} from '@cloudbeaver/plugin-data-viewer'; -import { DataGridContextMenuCellEditingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService'; -import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService'; -import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService'; -import { DataGridContextMenuSaveContentService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService'; -import { DataGridContextMenuService } from './DataGrid/DataGridContextMenu/DataGridContextMenuService'; -import { DataGridSettingsService } from './DataGridSettingsService'; -import { SpreadsheetGrid } from './SpreadsheetGrid'; +import { DataGridContextMenuCellEditingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.js'; +import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.js'; +import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.js'; +import { DataGridContextMenuSaveContentService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.js'; +import { DataGridSettingsService } from './DataGridSettingsService.js'; -@injectable() +const VALUE_TEXT_PRESENTATION_ID = 'value-text-presentation'; + +const SpreadsheetGrid = importLazyComponent(() => import('./SpreadsheetGrid.js').then(m => m.SpreadsheetGrid)); + +@injectable(() => [ + DataPresentationService, + DataGridSettingsService, + DataGridContextMenuOrderService, + DataGridContextMenuFilterService, + DataGridContextMenuCellEditingService, + DataGridContextMenuSaveContentService, + ActionService, + MenuService, + ExceptionsCatcherService, +]) export class SpreadsheetBootstrap extends Bootstrap { constructor( private readonly dataPresentationService: DataPresentationService, @@ -26,47 +50,98 @@ export class SpreadsheetBootstrap extends Bootstrap { private readonly dataGridContextMenuSortingService: DataGridContextMenuOrderService, private readonly dataGridContextMenuFilterService: DataGridContextMenuFilterService, private readonly dataGridContextMenuCellEditingService: DataGridContextMenuCellEditingService, - private readonly dataGridContextMenuService: DataGridContextMenuService, private readonly dataGridContextMenuSaveContentService: DataGridContextMenuSaveContentService, + private readonly actionService: ActionService, + private readonly menuService: MenuService, exceptionsCatcherService: ExceptionsCatcherService, ) { super(); - exceptionsCatcherService.ignore('ResizeObserver loop limit exceeded'); // Produces by react-data-grid + exceptionsCatcherService.ignore('ResizeObserver loop completed with undelivered notifications.'); // Produces by react-data-grid } - register(): void | Promise { + override register(): void | Promise { this.dataPresentationService.add({ id: 'spreadsheet_grid', + order: 0, dataFormat: ResultDataFormat.Resultset, getPresentationComponent: () => SpreadsheetGrid, - hidden: () => - this.dataGridSettingsService.settings.isValueDefault('hidden') - ? this.dataGridSettingsService.deprecatedSettings.getValue('hidden') - : this.dataGridSettingsService.settings.getValue('hidden'), + hidden: () => this.dataGridSettingsService.hidden, title: 'Table', icon: 'table-icon-sm', }); + this.dataGridContextMenuSortingService.register(); this.dataGridContextMenuFilterService.register(); this.dataGridContextMenuCellEditingService.register(); this.dataGridContextMenuSaveContentService.register(); - this.dataGridContextMenuService.add(this.dataGridContextMenuService.getMenuToken(), { - id: 'view_value_panel', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; + this.menuService.addCreator({ + root: true, + menus: [MENU_DV_CONTEXT_MENU], + contexts: [DATA_CONTEXT_DV_SIMPLE, DATA_CONTEXT_DV_ACTIONS, DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + getItems: (context, items) => [ACTION_OPEN, ...items, ACTION_DELETE], + }); + + this.actionService.addHandler({ + id: 'data-grid-key-base-handler', + menus: [MENU_DV_CONTEXT_MENU], + contexts: [DATA_CONTEXT_DV_SIMPLE, DATA_CONTEXT_DV_ACTIONS, DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + getActionInfo: (context, action) => { + if (action === ACTION_OPEN) { + return { ...action.info, label: 'data_grid_table_open_value_panel', icon: 'value-panel' }; + } + + if (action === ACTION_DELETE) { + return { ...action.info, label: 'data_grid_table_delete_filters_and_orders', icon: 'erase' }; + } + + return action.info; }, - isHidden(context) { - return typeof context.data.actions.valuePresentationId === 'string' || context.data.simple; + isActionApplicable: (context, action): boolean => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + if (action === ACTION_OPEN) { + const actions = context.get(DATA_CONTEXT_DV_ACTIONS); + const simple = context.get(DATA_CONTEXT_DV_SIMPLE); + + return actions?.valuePresentationId !== VALUE_TEXT_PRESENTATION_ID && !simple; + } + + if (action === ACTION_DELETE) { + const source = model.source as unknown as ResultSetDataSource; + + if (!isResultSetDataSource(model.source)) { + return false; + } + + const constraints = source.getAction(resultIndex, DatabaseDataConstraintAction); + return constraints.orderConstraints.length > 0 || constraints.filterConstraints.length > 0; + } + + return [ACTION_OPEN, ACTION_DELETE].includes(action); }, - order: 0.5, - title: 'data_grid_table_open_value_panel', - icon: 'value-panel', - onClick(context) { - context.data.actions.setValuePresentation(''); + handler: async (context, action) => { + if (action === ACTION_OPEN) { + const actions = context.get(DATA_CONTEXT_DV_ACTIONS); + + if (actions) { + actions.setValuePresentation(VALUE_TEXT_PRESENTATION_ID); + } + } + + if (action === ACTION_DELETE) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + const source = model.source as unknown as ResultSetDataSource; + const constraints = source.getAction(resultIndex, DatabaseDataConstraintAction); + + await model.request(() => { + constraints.deleteData(); + }); + } }, }); } - - load(): void | Promise {} } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetGrid.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetGrid.tsx index 96bda26fdd..3a277977b7 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetGrid.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetGrid.tsx @@ -1,14 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { IDataPresentationProps } from '@cloudbeaver/plugin-data-viewer'; -import { DataGridLoader } from './DataGrid/DataGridLoader'; +import { DataGridTable } from './DataGrid/DataGridTableLoader.js'; export const SpreadsheetGrid: React.FC = function SpreadsheetGrid(props) { - return ; + return ; }; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/index.ts b/webapp/packages/plugin-data-spreadsheet-new/src/index.ts index ab5e0e1f96..654f756665 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/index.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/index.ts @@ -1 +1,11 @@ -export * from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +export * from './manifest.js'; +export * from './DataGrid/DATA_GRID_BINDINGS.js'; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/de.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/de.ts new file mode 100644 index 0000000000..30c95bf1c7 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/de.ts @@ -0,0 +1,23 @@ +export default [ + ['data_grid_table_empty_placeholder', 'Die Tabelle enthält keine Spalten'], + ['data_grid_table_editing', 'Bearbeiten'], + ['data_grid_table_editing_set_to_null', 'Auf NULL einstellen'], + ['data_grid_table_editing_row_add', 'Zeile hinzufügen'], + ['data_grid_table_editing_row_add_copy', 'Doppelte Zeile'], + ['data_grid_table_editing_row_delete', 'Aktuelle Zeile löschen'], + ['data_grid_table_editing_row_revert', 'Wert zurückkehren'], + ['data_grid_table_order', 'Sortierung'], + ['data_grid_table_filter_cell_value', 'Zellwert'], + ['data_grid_table_filter_reset_all_filters', 'Alle Filter zurücksetzen'], + ['data_grid_table_disable_order', 'Deaktiviert'], + ['data_grid_table_disable_all_orders', 'Alle deaktivieren'], + ['data_grid_table_delete_filters_and_orders', 'Filter / Sortierung zurücksetzen'], + ['data_grid_table_tooltip_column_header_order', 'Sortieren nach Spalte'], + ['data_grid_table_context_menu_filter_dialog_title', 'Wert bearbeiten'], + ['data_grid_table_index_column_tooltip', 'Wählen ganze Tabelle aus'], + ['data_grid_table_readonly_tooltip', 'Schreibgeschützt'], + ['plugin_data_spreadsheet_new_settings_disable', 'Tabellenpräsentation deaktivieren'], + ['plugin_data_spreadsheet_new_settings_disable_description', 'Deaktivieren Sie die Tabellenpräsentation von Daten für alle Benutzer'], + ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], + ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], +]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts index d2b854a353..829288eab2 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts @@ -22,4 +22,8 @@ export default [ ['data_grid_table_context_menu_save_value_error', 'Failed to save value'], ['data_grid_table_index_column_tooltip', 'Select whole table'], ['data_grid_table_readonly_tooltip', 'Read-only'], + ['plugin_data_spreadsheet_new_settings_disable', 'Disable Table presentation'], + ['plugin_data_spreadsheet_new_settings_disable_description', 'Disable table presentation of data for all users'], + ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], + ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/fr.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/fr.ts new file mode 100644 index 0000000000..faef532200 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/fr.ts @@ -0,0 +1,28 @@ +export default [ + ['data_grid_table_empty_placeholder', 'La table ne contient aucune colonne'], + ['data_grid_table_editing', 'Modifier'], + ['data_grid_table_editing_set_to_null', 'Définir à NULL'], + ['data_grid_table_editing_open_inline_editor', "Ouvrir l'éditeur en ligne"], + ['data_grid_table_editing_row_add', 'Ajouter une ligne'], + ['data_grid_table_editing_row_add_copy', 'Dupliquer la ligne'], + ['data_grid_table_editing_row_delete', 'Supprimer la ligne actuelle'], + ['data_grid_table_editing_row_revert', 'Revenir à la valeur'], + ['data_grid_table_order', 'Tri'], + ['data_grid_table_open_value_panel', 'Afficher dans le panneau de valeurs'], + ['data_grid_table_filter', 'Filtres'], + ['data_grid_table_filter_cell_value', 'Valeur de la cellule'], + ['data_grid_table_filter_custom_value', 'Personnalisé'], + ['data_grid_table_filter_reset_all_filters', 'Réinitialiser tous les filtres'], + ['data_grid_table_disable_order', 'Désactivé'], + ['data_grid_table_disable_all_orders', 'Désactiver tout'], + ['data_grid_table_delete_filters_and_orders', 'Réinitialiser les filtres / le tri'], + ['data_grid_table_tooltip_column_header_order', 'Trier par colonne'], + ['data_grid_table_context_menu_filter_dialog_title', 'Modifier la valeur'], + ['data_grid_table_context_menu_filter_clipboard_permission', 'Donner accès au presse-papiers'], + ['data_grid_table_context_menu_save_value_error', 'Échec de la sauvegarde de la valeur'], + ['data_grid_table_index_column_tooltip', 'Sélectionner toute la table'], + ['data_grid_table_readonly_tooltip', 'Lecture seule'], + ['plugin_data_spreadsheet_new_settings_disable', 'Désactiver la présentation de la table'], + ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], + ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], +]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts index 53bc025695..d9d8896558 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts @@ -17,4 +17,7 @@ export default [ ['data_grid_table_context_menu_save_value_error', 'Failed to save value'], ['data_grid_table_index_column_tooltip', 'Seleziona tutta la tabella'], ['data_grid_table_readonly_tooltip', 'In sola lettura'], + ['plugin_data_spreadsheet_new_settings_disable', 'Disable Table presentation'], + ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], + ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts index 360fd1267c..483c9fad16 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts @@ -22,4 +22,8 @@ export default [ ['data_grid_table_context_menu_save_value_error', 'Не удалось сохранить значение'], ['data_grid_table_index_column_tooltip', 'Выбрать всю таблицу'], ['data_grid_table_readonly_tooltip', 'Доступно только для чтения'], + ['plugin_data_spreadsheet_new_settings_disable', 'Отключить табличное представление'], + ['plugin_data_spreadsheet_new_settings_disable_description', 'Отключить табличное представление данных для всех пользователей'], + ['plugin_data_spreadsheet_new_settings_description_label', 'Показать описание колонки'], + ['plugin_data_spreadsheet_new_settings_description_label_description', 'Описание будет показано под именами колонок в заголовке таблицы'], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/vi.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/vi.ts new file mode 100644 index 0000000000..4eda8ba15c --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/vi.ts @@ -0,0 +1,29 @@ +export default [ + ['data_grid_table_empty_placeholder', 'Bảng không chứa cột nào'], + ['data_grid_table_editing', 'Chỉnh sửa'], + ['data_grid_table_editing_set_to_null', 'Đặt thành NULL'], + ['data_grid_table_editing_open_inline_editor', 'Mở trình chỉnh sửa nội tuyến'], + ['data_grid_table_editing_row_add', 'Thêm hàng'], + ['data_grid_table_editing_row_add_copy', 'Sao chép hàng'], + ['data_grid_table_editing_row_delete', 'Xóa hàng hiện tại'], + ['data_grid_table_editing_row_revert', 'Hoàn nguyên giá trị'], + ['data_grid_table_order', 'Sắp xếp'], + ['data_grid_table_open_value_panel', 'Hiển thị trong bảng giá trị'], + ['data_grid_table_filter', 'Bộ lọc'], + ['data_grid_table_filter_cell_value', 'Giá trị ô'], + ['data_grid_table_filter_custom_value', 'Tùy chỉnh'], + ['data_grid_table_filter_reset_all_filters', 'Đặt lại tất cả bộ lọc'], + ['data_grid_table_disable_order', 'Đã tắt'], + ['data_grid_table_disable_all_orders', 'Tắt tất cả'], + ['data_grid_table_delete_filters_and_orders', 'Đặt lại bộ lọc / sắp xếp'], + ['data_grid_table_tooltip_column_header_order', 'Sắp xếp theo cột'], + ['data_grid_table_context_menu_filter_dialog_title', 'Chỉnh sửa giá trị'], + ['data_grid_table_context_menu_filter_clipboard_permission', 'Cấp quyền truy cập vào clipboard'], + ['data_grid_table_context_menu_save_value_error', 'Không thể lưu giá trị'], + ['data_grid_table_index_column_tooltip', 'Chọn toàn bộ bảng'], + ['data_grid_table_readonly_tooltip', 'Chỉ đọc'], + ['plugin_data_spreadsheet_new_settings_disable', 'Tắt chế độ hiển thị dạng bảng'], + ['plugin_data_spreadsheet_new_settings_disable_description', 'Tắt chế độ hiển thị dữ liệu dạng bảng cho tất cả người dùng'], + ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], + ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], +]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts index 37bb8b2298..51b94fd37b 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts @@ -4,7 +4,7 @@ export default [ ['data_grid_table_editing_set_to_null', '设置为NULL'], ['data_grid_table_editing_open_inline_editor', '打开內联编辑器'], ['data_grid_table_editing_row_add', '添加行'], - ['data_grid_table_editing_row_add_copy', 'Duplicate row'], + ['data_grid_table_editing_row_add_copy', '复制并添加行'], ['data_grid_table_editing_row_delete', '删除当前行'], ['data_grid_table_editing_row_revert', '还原值'], ['data_grid_table_order', '排序'], @@ -19,7 +19,10 @@ export default [ ['data_grid_table_tooltip_column_header_order', '按列排序'], ['data_grid_table_context_menu_filter_dialog_title', '编辑值'], ['data_grid_table_context_menu_filter_clipboard_permission', '授予访问剪贴板的权限'], - ['data_grid_table_context_menu_save_value_error', 'Failed to save value'], + ['data_grid_table_context_menu_save_value_error', '保存失败'], ['data_grid_table_index_column_tooltip', '选择整个表'], ['data_grid_table_readonly_tooltip', '只读'], + ['plugin_data_spreadsheet_new_settings_disable', '禁用表显示'], + ['plugin_data_spreadsheet_new_settings_description_label', 'Show columns description'], + ['plugin_data_spreadsheet_new_settings_description_label_description', 'Description will be shown under the column names in the table header'], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts b/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts index e1238fe1c9..133186a9f8 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts @@ -1,31 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { DataGridContextMenuCellEditingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService'; -import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService'; -import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService'; -import { DataGridContextMenuSaveContentService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService'; -import { DataGridContextMenuService } from './DataGrid/DataGridContextMenu/DataGridContextMenuService'; -import { DataGridSettingsService } from './DataGridSettingsService'; -import { LocaleService } from './LocaleService'; -import { SpreadsheetBootstrap } from './SpreadsheetBootstrap'; - export const dataSpreadsheetNewManifest: PluginManifest = { info: { name: 'New spreadsheet implementation' }, - providers: [ - SpreadsheetBootstrap, - DataGridSettingsService, - LocaleService, - DataGridContextMenuService, - DataGridContextMenuOrderService, - DataGridContextMenuFilterService, - DataGridContextMenuCellEditingService, - DataGridContextMenuSaveContentService, - ], }; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/module.ts b/webapp/packages/plugin-data-spreadsheet-new/src/module.ts new file mode 100644 index 0000000000..bba1f5aaa4 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/module.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { LocaleService } from './LocaleService.js'; +import { SpreadsheetBootstrap } from './SpreadsheetBootstrap.js'; +import { DataGridSettingsService } from './DataGridSettingsService.js'; +import { DataGridContextMenuSaveContentService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.js'; +import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.js'; +import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.js'; +import { DataGridContextMenuCellEditingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-data-spreadsheet-new', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Dependency, proxy(DataGridSettingsService)) + .addSingleton(Bootstrap, SpreadsheetBootstrap) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(DataGridContextMenuCellEditingService) + .addSingleton(DataGridSettingsService) + .addSingleton(DataGridContextMenuSaveContentService) + .addSingleton(DataGridContextMenuOrderService) + .addSingleton(DataGridContextMenuFilterService); + }, +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss b/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss deleted file mode 100644 index 2d512b83a9..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss +++ /dev/null @@ -1,51 +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. - */ - -.cb-react-grid-container { - outline: 0; - display: flex; - flex-direction: column; - flex: 1; - overflow: auto; - user-select: none; - -webkit-user-select: none; - - .cb-react-grid-theme { - outline: 0 !important; - height: 100% !important; - background-color: inherit !important; - border: none !important; - font-family: inherit !important; - font-size: inherit !important; - - .rdg-header-row, - .rdg-row { - background-color: var(--theme-surface) !important; - } - * { - font-size: inherit !important; - font-family: inherit !important; - } - .rdg-cell.rdg-cell-frozen { - box-shadow: none !important; - } - - .rdg-cell-editing { - overflow: visible; - height: 24px; - box-shadow: none !important; - } - - .rdg-editor-container { - position: relative; - display: flex; - width: 100%; - height: 100%; - } - } -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/styles.ts b/webapp/packages/plugin-data-spreadsheet-new/src/styles/styles.ts deleted file mode 100644 index 700ef7dcb5..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/styles.ts +++ /dev/null @@ -1,23 +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 type { ThemeSelector } from '@cloudbeaver/core-theming'; - -export const reactGridStyles: ThemeSelector = async theme => { - let styles: any; - - switch (theme) { - case 'dark': - styles = await import('./themes/dark.module.scss'); - break; - default: - styles = await import('./themes/light.module.scss'); - break; - } - - return [styles.default]; -}; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss b/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss deleted file mode 100644 index ef78221205..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss +++ /dev/null @@ -1,143 +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 '@cloudbeaver/core-theming/src/styles/branding'; - -$edited-color: rgba(255, 153, 0, 0.3); -$added-color: rgba(145, 255, 0, 0.3); -$deleted-color: rgba(255, 51, 0, 0.3); - -@mixin base-react-grid() { - :global { - .cb-react-grid-container { - @include mdc-typography(caption); - - :global(input) { - @include mdc-typography(caption); - } - } - .cb-react-grid-container:focus-within { - .rdg-cell-custom-selected::before { - background-color: rgba(0, 145, 234, 0.2); - } - - .rdg-cell:global([aria-selected='true']) { - box-shadow: inset 0 0 0 1px #0091ea !important; - } - - .rdg-cell-custom-editing { - box-shadow: none !important; - - &::before { - background-color: transparent; - } - } - - .rdg-cell-custom-edited { - background-color: $edited-color !important; - } - - .rdg-cell-custom-added { - background-color: $added-color !important; - } - - .rdg-cell-custom-deleted { - background-color: $deleted-color !important; - } - } - .cb-react-grid-theme { - @include mdc-typography(caption); - @include mdc-theme-prop(color, on-surface, true); - - .rdg-table-header__readonly-status { - background-color: #e28835 !important; - @include mdc-theme-prop(border-color, surface, true); - } - - .rdg-table-header__order-button_unordered { - color: #c4c4c4 !important; - - &:hover { - color: $mdc-theme-primary !important; - } - } - - .rdg-header-row { - @include mdc-theme-prop(background-color, surface, true); - } - - .rdg-row { - @include mdc-theme-prop(border-color, background, true); - } - - .rdg-row:hover .rdg-cell, - .rdg-row:hover .rdg-cell-frozen { - border-bottom: 1px solid !important; - border-bottom-color: $color-positive !important; - } - - .rdg-row:hover { - @include stripes-background($mdc-theme-sub-secondary, true); - } - - .rdg-cell { - @include mdc-theme-prop(border-color, background, true); - - &:focus { - outline: 0 !important; - } - - &:global([aria-selected='true']) { - outline: 0 !important; - box-shadow: inset 0 0 0 1px #808080 !important; - } - } - - .rdg-cell-frozen { - @include mdc-theme-prop(background-color, surface, true); - } - - .rdg-cell-custom-selected { - box-shadow: none !important; - } - - .rdg-cell-custom-selected::before { - content: ''; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(150, 150, 150, 0.2); - } - - .rdg-cell-custom-editing { - box-shadow: none !important; - background-color: inherit !important; - } - - .rdg-cell-custom-edited { - background-color: $edited-color !important; - } - - .rdg-cell-custom-added { - background-color: $added-color !important; - } - - .rdg-cell-custom-deleted { - background-color: $deleted-color !important; - } - - .cell-formatter { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - } - } -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/dark.module.scss b/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/dark.module.scss deleted file mode 100644 index 8eb55d2612..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/dark.module.scss +++ /dev/null @@ -1,18 +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 '@cloudbeaver/core-theming/src/styles/theme-dark'; -@import 'base-react-grid'; - -:global .#{$theme-class} { - @include base-react-grid; - - .cb-react-grid-theme { - --rdg-color-scheme: dark; - } -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/light.module.scss b/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/light.module.scss deleted file mode 100644 index 8131f082f8..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/light.module.scss +++ /dev/null @@ -1,18 +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 '@cloudbeaver/core-theming/src/styles/theme-light'; -@import 'base-react-grid'; - -:global .#{$theme-class} { - @include base-react-grid; - - .cb-react-grid-theme { - --rdg-color-scheme: light; - } -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json index 04b4f33a00..8f7ea953aa 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json +++ b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json @@ -1,55 +1,74 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-data-viewer/tsconfig.json" + "path": "../../common-react/@dbeaver/react-tests" }, { - "path": "../plugin-react-data-grid/tsconfig.json" + "path": "../../common-react/@dbeaver/ui-kit" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../../common-typescript/@dbeaver/cli" }, { - "path": "../core-data-context/tsconfig.json" + "path": "../../common-typescript/@dbeaver/js-helpers" }, { - "path": "../core-di/tsconfig.json" + "path": "../../common-typescript/@dbeaver/result-set-api" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-browser" }, { - "path": "../core-executor/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-executor" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-localization" }, { - "path": "../tests-runner/tsconfig.json" + "path": "../core-sdk" + }, + { + "path": "../core-settings" + }, + { + "path": "../core-ui" + }, + { + "path": "../core-utils" + }, + { + "path": "../core-view" + }, + { + "path": "../plugin-data-grid" + }, + { + "path": "../plugin-data-viewer" } ], "include": [ @@ -61,7 +80,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } 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 e533f68fa4..1e15548ea3 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/package.json +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-data-viewer-result-set-grouping", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,33 +11,40 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-data-viewer": "~0.1.0", - "@cloudbeaver/plugin-sql-editor": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-data-context": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-data-viewer": "workspace:*", + "@cloudbeaver/plugin-sql-editor": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CLEAR.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CLEAR.ts index f02153bbc3..ce916549a8 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CLEAR.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CLEAR.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CONFIGURE.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CONFIGURE.ts index 4e8022c119..ae90b6367d 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CONFIGURE.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CONFIGURE.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN.ts index 3c38d776b8..8cf0fdb859 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES.ts index 708c9754c3..20cb1d054f 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DEFAULT_GROUPING_QUERY_OPERATION.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DEFAULT_GROUPING_QUERY_OPERATION.ts index 3e9d744aac..07b1734997 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DEFAULT_GROUPING_QUERY_OPERATION.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DEFAULT_GROUPING_QUERY_OPERATION.ts @@ -1 +1,8 @@ -export const DEFAULT_GROUPING_QUERY_OPERATION = 'COUNT(*)'; \ No newline at end of file +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +export const DEFAULT_GROUPING_QUERY_OPERATION = 'COUNT(*)'; diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog.m.css b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog.m.css deleted file mode 100644 index 84a947fc34..0000000000 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog.m.css +++ /dev/null @@ -1,6 +0,0 @@ -.footerContainer { - width: 100%; - display: flex; - gap: 14px; - justify-content: flex-end; -} \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog.module.css b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog.module.css new file mode 100644 index 0000000000..12dbe71fec --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog.module.css @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.footerContainer { + width: 100%; + display: flex; + gap: 14px; + justify-content: flex-end; +} diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog.tsx b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog.tsx index 9dac44ff19..1f7508e39f 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog.tsx +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -22,9 +22,9 @@ import { } from '@cloudbeaver/core-blocks'; import type { DialogComponentProps } from '@cloudbeaver/core-dialogs'; -import type { IResultSetGroupingData } from '../DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING'; -import styles from './DVGroupingColumnEditorDialog.m.css'; -import { GroupingColumnEditorTable } from './GroupingColumnEditorTable'; +import type { IResultSetGroupingData } from '../DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING.js'; +import styles from './DVGroupingColumnEditorDialog.module.css'; +import { GroupingColumnEditorTable } from './GroupingColumnEditorTable.js'; interface Payload { grouping: IResultSetGroupingData; @@ -93,12 +93,10 @@ export const DVGroupingColumnEditorDialog = observer
- - +
diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingColumnEditorTable.m.css b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingColumnEditorTable.m.css deleted file mode 100644 index 12059f317f..0000000000 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingColumnEditorTable.m.css +++ /dev/null @@ -1,19 +0,0 @@ -.header { - composes: theme-border-color-background theme-background-surface theme-text-on-surface from global; - padding-bottom: 16px; - gap: 16px; - border-bottom: 1px solid; -} -.headerActions { - width: 100%; - display: flex; - gap: 16px; -} - -.tableContainer { - height: 200px; -} - -.inputField { - flex: 1; -} \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingColumnEditorTable.module.css b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingColumnEditorTable.module.css new file mode 100644 index 0000000000..4460993216 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingColumnEditorTable.module.css @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.header { + composes: theme-border-color-background theme-background-surface theme-text-on-surface from global; + padding-bottom: 16px; + gap: 16px; + border-bottom: 1px solid; +} +.headerActions { + width: 100%; + display: flex; + gap: 16px; +} + +.tableContainer { + height: 200px; +} + +.inputField { + flex: 1; +} diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingColumnEditorTable.tsx b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingColumnEditorTable.tsx index 15263189d7..f6f4480e3f 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingColumnEditorTable.tsx +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingColumnEditorTable.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,8 +10,8 @@ import { useState } from 'react'; import { Button, Container, Form, Group, GroupTitle, InputField, s, Table, TableBody, useS } from '@cloudbeaver/core-blocks'; -import styles from './GroupingColumnEditorTable.m.css'; -import { GroupingTableItem } from './GroupingTableItem'; +import styles from './GroupingColumnEditorTable.module.css'; +import { GroupingTableItem } from './GroupingTableItem.js'; interface Props { title: string; @@ -45,9 +45,7 @@ export const GroupingColumnEditorTable = observer(function GroupingColumn value={newColumnName} onChange={v => setNewColumnName(String(v))} /> - + diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingTableItem.tsx b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingTableItem.tsx index fc5c4ea282..b01f1b26d0 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingTableItem.tsx +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVGroupingColumnEditorDialog/GroupingTableItem.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPluginBootstrap.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPluginBootstrap.ts index e2aaadda92..f46602976d 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPluginBootstrap.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPluginBootstrap.ts @@ -1,33 +1,45 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view'; +import { ActionService, MenuService } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_PRESENTATION, DATA_VIEWER_DATA_MODEL_ACTIONS_MENU, + DatabaseDataResultAction, DataPresentationService, DataPresentationType, + DataViewerPresentationType, + type IDatabaseDataModel, + isResultSetDataSource, ResultSetDataAction, + ResultSetDataSource, ResultSetSelectAction, } from '@cloudbeaver/plugin-data-viewer'; -import { ACTION_DATA_VIEWER_GROUPING_CLEAR } from './Actions/ACTION_DATA_VIEWER_GROUPING_CLEAR'; -import { ACTION_DATA_VIEWER_GROUPING_CONFIGURE } from './Actions/ACTION_DATA_VIEWER_GROUPING_CONFIGURE'; -import { ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN } from './Actions/ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN'; -import { ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES } from './Actions/ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES'; -import { DATA_CONTEXT_DV_DDM_RS_GROUPING } from './DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING'; -import { DVGroupingColumnEditorDialog } from './DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog'; -import { DVResultSetGroupingPresentation } from './DVResultSetGroupingPresentation'; +import { ACTION_DATA_VIEWER_GROUPING_CLEAR } from './Actions/ACTION_DATA_VIEWER_GROUPING_CLEAR.js'; +import { ACTION_DATA_VIEWER_GROUPING_CONFIGURE } from './Actions/ACTION_DATA_VIEWER_GROUPING_CONFIGURE.js'; +import { ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN } from './Actions/ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN.js'; +import { ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES } from './Actions/ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES.js'; +import { DATA_CONTEXT_DV_DDM_RS_GROUPING } from './DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING.js'; -@injectable() +const DVGroupingColumnEditorDialog = importLazyComponent(() => + import('./DVGroupingColumnEditorDialog/DVGroupingColumnEditorDialog.js').then(module => module.DVGroupingColumnEditorDialog), +); +const DVResultSetGroupingPresentation = importLazyComponent(() => + import('./DVResultSetGroupingPresentation.js').then(module => module.DVResultSetGroupingPresentation), +); + +@injectable(() => [DataPresentationService, MenuService, ActionService, CommonDialogService]) export class DVResultSetGroupingPluginBootstrap extends Bootstrap { constructor( private readonly dataPresentationService: DataPresentationService, @@ -38,32 +50,40 @@ export class DVResultSetGroupingPluginBootstrap extends Bootstrap { super(); } - register(): void { + override register(): void { this.registerPresentation(); this.registerActions(); } - load(): void | Promise {} - private registerActions(): void { this.actionService.addHandler({ id: 'data-viewer-grouping-menu-base-handler', + actions: [ + ACTION_DATA_VIEWER_GROUPING_CLEAR, + ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN, + ACTION_DATA_VIEWER_GROUPING_CONFIGURE, + ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES, + ], + contexts: [DATA_CONTEXT_DV_DDM_RS_GROUPING], + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], isActionApplicable(context, action) { - const menu = context.hasValue(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_ACTIONS_MENU); - - if (!menu || !context.has(DATA_CONTEXT_DV_DDM_RS_GROUPING)) { + const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION); + const model = context.get(DATA_CONTEXT_DV_DDM)!; + if ((presentation && presentation.type !== DataViewerPresentationType.Data) || !isResultSetDataSource(model.source)) { return false; } - - return [ - ACTION_DATA_VIEWER_GROUPING_CLEAR, - ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN, - ACTION_DATA_VIEWER_GROUPING_CONFIGURE, - ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES, - ].includes(action); + switch (action) { + case ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN: + return context.has(DATA_CONTEXT_DV_DDM) && context.has(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + case ACTION_DATA_VIEWER_GROUPING_CLEAR: + case ACTION_DATA_VIEWER_GROUPING_CONFIGURE: + case ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES: + return true; + } + return false; }, getActionInfo(context, action) { - const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING); + const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING)!; const isShowDuplicatesOnly = grouping.getShowDuplicatesOnly(); if (action === ACTION_DATA_VIEWER_GROUPING_SHOW_DUPLICATES && isShowDuplicatesOnly) { @@ -78,44 +98,51 @@ export class DVResultSetGroupingPluginBootstrap extends Bootstrap { return action.info; }, isDisabled(context, action) { - const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING); - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING)!; switch (action) { case ACTION_DATA_VIEWER_GROUPING_CLEAR: return grouping.getColumns().length === 0; case ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN: { + const model = context.get(DATA_CONTEXT_DV_DDM)! as unknown as IDatabaseDataModel; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; if (!model.source.hasResult(resultIndex)) { return true; } - const selectionAction = model.source.getAction(resultIndex, ResultSetSelectAction); - const dataAction = model.source.getAction(resultIndex, ResultSetDataAction); - return !grouping.getColumns().some(name => { - const key = dataAction.findColumnKey(column => column.name === name); + const format = model.source.getResult(resultIndex)?.dataFormat; - if (!key) { - return false; - } + if (format === ResultDataFormat.Resultset) { + const selectionAction = model.source.getAction(resultIndex, ResultSetSelectAction); + const dataAction = model.source.getAction(resultIndex, ResultSetDataAction); - return selectionAction.isElementSelected({ column: key }); - }); + return !grouping.getColumns().some(name => { + const key = dataAction.findColumnKey(column => column.name === name); + + if (!key) { + return false; + } + + return selectionAction.isElementSelected({ column: key }); + }); + } + + return true; } } return false; }, handler: async (context, action) => { - const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING); - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + const grouping = context.get(DATA_CONTEXT_DV_DDM_RS_GROUPING)!; switch (action) { case ACTION_DATA_VIEWER_GROUPING_CLEAR: grouping.clear(); break; case ACTION_DATA_VIEWER_GROUPING_REMOVE_COLUMN: { + const model = context.get(DATA_CONTEXT_DV_DDM)! as unknown as IDatabaseDataModel; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; const selectionAction = model.source.getAction(resultIndex, ResultSetSelectAction); const dataAction = model.source.getAction(resultIndex, ResultSetDataAction); @@ -143,9 +170,7 @@ export class DVResultSetGroupingPluginBootstrap extends Bootstrap { }); this.menuService.addCreator({ menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], - isApplicable(context) { - return context.has(DATA_CONTEXT_DV_DDM_RS_GROUPING); - }, + contexts: [DATA_CONTEXT_DV_DDM_RS_GROUPING], getItems(context, items) { return [ ...items, @@ -166,11 +191,12 @@ export class DVResultSetGroupingPluginBootstrap extends Bootstrap { icon: '/icons/plugin_data_viewer_result_set_grouping_m.svg', dataFormat: ResultDataFormat.Resultset, hidden: (dataFormat, model, resultIndex) => { - if (!model.source.hasResult(resultIndex)) { + const source = model.source as any; + if (!isResultSetDataSource(source) || !source.hasResult(resultIndex)) { return true; } - const data = model.source.tryGetAction(resultIndex, ResultSetDataAction); + const data = source.getActionImplementation(resultIndex, DatabaseDataResultAction); return data?.empty ?? true; }, getPresentationComponent: () => DVResultSetGroupingPresentation, diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.module.css b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.module.css new file mode 100644 index 0000000000..7a7f97d5f7 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.module.css @@ -0,0 +1,73 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.dropArea { + composes: theme-background-secondary theme-text-on-secondary from global; + flex: 1; + display: flex; + position: relative; + overflow: auto; + + &.active::after, + &.negative::after { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: dashed 2px; + border-color: transparent; + border-radius: var(--theme-group-element-radius); + } + + &.active::after { + content: ''; + border-color: var(--theme-primary) !important; + } + + &.negative::after { + content: ''; + border-color: var(--theme-negative) !important; + } + + & .placeholder { + display: flex; + height: 100%; + width: 100%; + + & .message { + box-sizing: border-box; + padding: 24px; + margin: auto; + text-align: center; + white-space: pre-wrap; + } + } +} + +.throwBox { + position: fixed; + + &:not(.showDropOutside) { + left: 0; + top: 0; + height: 0; + width: 0; + } + + &.showDropOutside { + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: 999; + } +} + +.throwBox.showDropOutside + .dropArea { + z-index: 1000; +} diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx index 87feae22c0..24e43ad34b 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentation.tsx @@ -1,157 +1,91 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useContext, useState } from 'react'; -import styled, { css, use } from 'reshadow'; -import { useTranslate } from '@cloudbeaver/core-blocks'; -import { useDataContext } from '@cloudbeaver/core-data-context'; -import { useTabLocalState } from '@cloudbeaver/core-ui'; -import { CaptureViewContext } from '@cloudbeaver/core-view'; -import { DataPresentationComponent, IDatabaseResultSet, TableViewerLoader } from '@cloudbeaver/plugin-data-viewer'; +import { s, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { CaptureViewScope } from '@cloudbeaver/core-view'; +import { DatabaseMetadataAction, type DataPresentationComponent, isResultSetDataModel, TableViewerLoader } from '@cloudbeaver/plugin-data-viewer'; -import { DATA_CONTEXT_DV_DDM_RS_GROUPING } from './DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING'; -import { DEFAULT_GROUPING_QUERY_OPERATION } from './DEFAULT_GROUPING_QUERY_OPERATION'; -import type { IGroupingQueryState } from './IGroupingQueryState'; -import { useGroupingData } from './useGroupingData'; -import { useGroupingDataModel } from './useGroupingDataModel'; -import { useGroupingDnDColumns } from './useGroupingDnDColumns'; +import { DEFAULT_GROUPING_QUERY_OPERATION } from './DEFAULT_GROUPING_QUERY_OPERATION.js'; +import styles from './DVResultSetGroupingPresentation.module.css'; +import { DVResultSetGroupingPresentationContext } from './DVResultSetGroupingPresentationContext.js'; +import type { IDVResultSetGroupingPresentationState } from './IDVResultSetGroupingPresentationState.js'; +import { useGroupingDataModel } from './useGroupingDataModel.js'; +import { useGroupingDnDColumns } from './useGroupingDnDColumns.js'; -const styles = css` - drop-area { - composes: theme-background-secondary theme-text-on-secondary from global; - flex: 1; - display: flex; - position: relative; - overflow: auto; - - &[|active]::after, - &[|negative]::after { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border: dashed 2px; - border-color: transparent; - border-radius: var(--theme-group-element-radius); - } - - &[|active]::after { - content: ''; - border-color: var(--theme-primary) !important; - } - - &[|negative]::after { - content: ''; - border-color: var(--theme-negative) !important; - } - - & placeholder { - display: flex; - height: 100%; - width: 100%; - - & message { - box-sizing: border-box; - padding: 24px; - margin: auto; - text-align: center; - white-space: pre-wrap; - } - } - } - - throw-box { - position: fixed; - - &:not([|showDropOutside]) { - left: 0; - top: 0; - height: 0; - width: 0; - } - - &[|showDropOutside] { - left: 0; - top: 0; - right: 0; - bottom: 0; - z-index: 999; - } - } - - throw-box[|showDropOutside] + drop-area { - z-index: 1000; - } -`; - -export interface IDVResultSetGroupingPresentationState extends IGroupingQueryState { - presentationId: string; -} - -export const DVResultSetGroupingPresentation: DataPresentationComponent = observer(function DVResultSetGroupingPresentation({ - model: originalModel, +export const DVResultSetGroupingPresentation: DataPresentationComponent = observer(function DVResultSetGroupingPresentation({ + model: unknownModel, resultIndex, }) { - const state = useTabLocalState(() => ({ - presentationId: '', - columns: [], - functions: [DEFAULT_GROUPING_QUERY_OPERATION], - showDuplicatesOnly: false, - })); + const originalModel = unknownModel as any; + if (!isResultSetDataModel(originalModel)) { + throw new Error('DVResultSetGroupingPresentation can only be used with ResultSetDataSource'); + } + const metadataAction = originalModel.source.getAction(resultIndex, DatabaseMetadataAction); + + const state = metadataAction.get(`grouping-panel-${originalModel.id}`, () => + observable({ + presentationId: '', + valuePresentationId: null, + columns: [], + functions: [DEFAULT_GROUPING_QUERY_OPERATION], + showDuplicatesOnly: false, + modelId: '', + }), + ); + + const style = useS(styles); - const viewContext = useContext(CaptureViewContext); - const context = useDataContext(viewContext); const translate = useTranslate(); - const [presentationId, setPresentation] = useState(''); - const [valuePresentationId, setValuePresentation] = useState(null); const model = useGroupingDataModel(originalModel, resultIndex, state); const dnd = useGroupingDnDColumns(state, originalModel, model); - const grouping = useGroupingData(state); - - context.set(DATA_CONTEXT_DV_DDM_RS_GROUPING, grouping); - - return styled(styles)( - <> - + +
- {state.columns.length === 0 ? ( - - {translate('plugin_data_viewer_result_set_grouping_placeholder')} - +
+
{translate('plugin_data_viewer_result_set_grouping_placeholder')}
+
) : ( { + state.presentationId = presentationId; + }} + onValuePresentationChange={presentationId => { + state.valuePresentationId = presentationId; + }} /> )} -
- , +
+ ); }); diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentationContext.tsx b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentationContext.tsx new file mode 100644 index 0000000000..4e73fc9531 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DVResultSetGroupingPresentationContext.tsx @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { useCaptureViewContext } from '@cloudbeaver/core-view'; + +import { DATA_CONTEXT_DV_DDM_RS_GROUPING } from './DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING.js'; +import type { IDVResultSetGroupingPresentationState } from './IDVResultSetGroupingPresentationState.js'; +import { useGroupingData } from './useGroupingData.js'; + +interface Props { + state: IDVResultSetGroupingPresentationState; +} + +export const DVResultSetGroupingPresentationContext = observer(function DVResultSetGroupingPresentationContext({ state }) { + const grouping = useGroupingData(state); + + useCaptureViewContext((context, id) => { + context.set(DATA_CONTEXT_DV_DDM_RS_GROUPING, grouping, id); + }); + + return null; +}); diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING.ts index 5f7bba7c9d..19e0a4b7ac 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/GroupingDataSource.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/GroupingDataSource.ts index fefcf034f2..a946f26598 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/GroupingDataSource.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/GroupingDataSource.ts @@ -1,12 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { IDatabaseResultSet } from '@cloudbeaver/plugin-data-viewer'; -import { IDataQueryOptions, QueryDataSource } from '@cloudbeaver/plugin-sql-editor'; +import { type IDataQueryOptions, QueryDataSource } from '@cloudbeaver/plugin-sql-editor'; export interface IDataGroupingOptions extends IDataQueryOptions { query: string; @@ -17,7 +17,7 @@ export interface IDataGroupingOptions extends IDataQueryOptions { } export class GroupingDataSource extends QueryDataSource { - async request(prevResults: IDatabaseResultSet[]): Promise { + override async request(prevResults: IDatabaseResultSet[]): Promise { await this.generateQuery(); return await super.request(prevResults); } diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/IDVResultSetGroupingPresentationState.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/IDVResultSetGroupingPresentationState.ts new file mode 100644 index 0000000000..64da201e06 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/IDVResultSetGroupingPresentationState.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IGroupingQueryState } from './IGroupingQueryState.js'; + +export interface IDVResultSetGroupingPresentationState extends IGroupingQueryState { + presentationId: string; + valuePresentationId: string | null; + modelId: string; +} diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/IGroupingQueryState.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/IGroupingQueryState.ts index 237fe7f208..1996169a63 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/IGroupingQueryState.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/IGroupingQueryState.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/LocaleService.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/LocaleService.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/index.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/index.ts index dc9e65d40b..5672b0a7ae 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/index.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/index.ts @@ -1,4 +1,13 @@ -import { dvResultSetGroupingPlugin } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { dvResultSetGroupingPlugin } from './manifest.js'; export { dvResultSetGroupingPlugin }; export default dvResultSetGroupingPlugin; diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/locales/fr.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/locales/fr.ts new file mode 100644 index 0000000000..ee170038ae --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/locales/fr.ts @@ -0,0 +1,17 @@ +export default [ + ['plugin_data_viewer_result_set_grouping_title', 'Regroupement'], + [ + 'plugin_data_viewer_result_set_grouping_placeholder', + "Il n'y a pas de données à afficher.\nFaites glisser une colonne de l'affichage des résultats pour regrouper les valeurs.", + ], + ['plugin_data_viewer_result_set_grouping_column_delete_tooltip', 'Supprimer le regroupement par colonnes sélectionnées'], + ['plugin_data_viewer_result_set_grouping_action_show_duplicates', 'Afficher les doublons'], + ['plugin_data_viewer_result_set_grouping_action_show_all', 'Afficher tout'], + ['plugin_data_viewer_result_set_grouping_action_configure', 'Configurer'], + ['plugin_data_viewer_result_set_grouping_action_configure_tooltip', 'Modifier la configuration du regroupement'], + ['plugin_data_viewer_result_set_grouping_grouping_configuration', 'Configuration du regroupement'], + ['plugin_data_viewer_result_set_grouping_grouping_columns', 'Colonnes'], + ['plugin_data_viewer_result_set_grouping_grouping_functions', 'Fonctions'], + ['plugin_data_viewer_result_set_grouping_grouping_columns_placeholder', 'Entrez le nom de la nouvelle colonne'], + ['plugin_data_viewer_result_set_grouping_grouping_functions_placeholder', 'Entrez la fonction (par exemple, SUM(salary), AVG(score))'], +]; diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/locales/vi.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/locales/vi.ts new file mode 100644 index 0000000000..9e0b293bf5 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/locales/vi.ts @@ -0,0 +1,17 @@ +export default [ + ['plugin_data_viewer_result_set_grouping_title', 'Nhóm'], + [ + 'plugin_data_viewer_result_set_grouping_placeholder', + 'Không có dữ liệu để hiển thị.\nKéo và thả một cột từ trình xem kết quả để nhóm các giá trị.', + ], + ['plugin_data_viewer_result_set_grouping_column_delete_tooltip', 'Xóa nhóm theo các cột đã chọn'], + ['plugin_data_viewer_result_set_grouping_action_show_duplicates', 'Hiển thị các giá trị trùng lặp'], + ['plugin_data_viewer_result_set_grouping_action_show_all', 'Hiển thị tất cả'], + ['plugin_data_viewer_result_set_grouping_action_configure', 'Cấu hình'], + ['plugin_data_viewer_result_set_grouping_action_configure_tooltip', 'Chỉnh sửa cấu hình nhóm'], + ['plugin_data_viewer_result_set_grouping_grouping_configuration', 'Cấu hình nhóm'], + ['plugin_data_viewer_result_set_grouping_grouping_columns', 'Cột'], + ['plugin_data_viewer_result_set_grouping_grouping_functions', 'Hàm'], + ['plugin_data_viewer_result_set_grouping_grouping_columns_placeholder', 'Nhập tên cột mới'], + ['plugin_data_viewer_result_set_grouping_grouping_functions_placeholder', 'Nhập hàm (ví dụ: SUM(salary), AVG(score))'], +]; diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/locales/zh.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/locales/zh.ts index 73bbec4faa..90ee327401 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/locales/zh.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/locales/zh.ts @@ -1,14 +1,14 @@ export default [ - ['plugin_data_viewer_result_set_grouping_title', 'Grouping'], - ['plugin_data_viewer_result_set_grouping_placeholder', 'There is no data to show.\nDrag and drop a column from the result viewer to group values.'], - ['plugin_data_viewer_result_set_grouping_column_delete_tooltip', 'Remove grouping by selected columns'], - ['plugin_data_viewer_result_set_grouping_action_show_duplicates', 'Show duplicates'], - ['plugin_data_viewer_result_set_grouping_action_show_all', 'Show all'], - ['plugin_data_viewer_result_set_grouping_action_configure', 'Configure'], - ['plugin_data_viewer_result_set_grouping_action_configure_tooltip', 'Edit grouping configuration'], - ['plugin_data_viewer_result_set_grouping_grouping_configuration', 'Grouping configuration'], - ['plugin_data_viewer_result_set_grouping_grouping_columns', 'Columns'], - ['plugin_data_viewer_result_set_grouping_grouping_functions', 'Functions'], - ['plugin_data_viewer_result_set_grouping_grouping_columns_placeholder', 'Enter new column name'], - ['plugin_data_viewer_result_set_grouping_grouping_functions_placeholder', 'Enter function (e.g., SUM(salary), AVG(score))'], + ['plugin_data_viewer_result_set_grouping_title', '分组'], + ['plugin_data_viewer_result_set_grouping_placeholder', '无显示结果。\n从结果查看器中拖放列以对值进行分组。'], + ['plugin_data_viewer_result_set_grouping_column_delete_tooltip', '移除所选列分组'], + ['plugin_data_viewer_result_set_grouping_action_show_duplicates', '查看重复'], + ['plugin_data_viewer_result_set_grouping_action_show_all', '查看全部'], + ['plugin_data_viewer_result_set_grouping_action_configure', '配置'], + ['plugin_data_viewer_result_set_grouping_action_configure_tooltip', '编辑分组配置'], + ['plugin_data_viewer_result_set_grouping_grouping_configuration', '分组配置'], + ['plugin_data_viewer_result_set_grouping_grouping_columns', '列'], + ['plugin_data_viewer_result_set_grouping_grouping_functions', '函数'], + ['plugin_data_viewer_result_set_grouping_grouping_columns_placeholder', '输入新列名'], + ['plugin_data_viewer_result_set_grouping_grouping_functions_placeholder', '输入函数 (如 SUM(salary), AVG(score))'], ]; diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/manifest.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/manifest.ts index 1e0732d10b..ad8ee3e690 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/manifest.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/manifest.ts @@ -1,16 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { DVResultSetGroupingPluginBootstrap } from './DVResultSetGroupingPluginBootstrap'; -import { LocaleService } from './LocaleService'; - export const dvResultSetGroupingPlugin: PluginManifest = { info: { name: 'Result Set Grouping plugin' }, - providers: [DVResultSetGroupingPluginBootstrap, LocaleService], }; diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/module.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/module.ts new file mode 100644 index 0000000000..bcfd96cd61 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/module.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ModuleRegistry, Bootstrap } from '@cloudbeaver/core-di'; +import { LocaleService } from './LocaleService.js'; +import { DVResultSetGroupingPluginBootstrap } from './DVResultSetGroupingPluginBootstrap.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-data-viewer-result-set-grouping', + + configure: serviceCollection => { + serviceCollection.addSingleton(Bootstrap, DVResultSetGroupingPluginBootstrap).addSingleton(Bootstrap, LocaleService); + }, +}); diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingData.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingData.ts index 7f5524b6bc..189b2fa62a 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingData.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingData.ts @@ -1,10 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ import { action } from 'mobx'; import { useObservableRef } from '@cloudbeaver/core-blocks'; -import type { IResultSetGroupingData } from './DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING'; -import type { IDVResultSetGroupingPresentationState } from './DVResultSetGroupingPresentation'; -import { DEFAULT_GROUPING_QUERY_OPERATION } from './DEFAULT_GROUPING_QUERY_OPERATION'; +import type { IResultSetGroupingData } from './DataContext/DATA_CONTEXT_DV_DDM_RS_GROUPING.js'; +import { DEFAULT_GROUPING_QUERY_OPERATION } from './DEFAULT_GROUPING_QUERY_OPERATION.js'; +import type { IDVResultSetGroupingPresentationState } from './IDVResultSetGroupingPresentationState.js'; export interface IPrivateGroupingData extends IResultSetGroupingData { state: IDVResultSetGroupingPresentationState; diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDataModel.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDataModel.ts index 270ddf0bf8..32ae14f648 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDataModel.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDataModel.ts @@ -1,41 +1,43 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { reaction } from 'mobx'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useObjectRef, useResource } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; -import { App, useService } from '@cloudbeaver/core-di'; -import { AsyncTaskInfoService, GraphQLService } from '@cloudbeaver/core-sdk'; +import { IServiceProvider, useService } from '@cloudbeaver/core-di'; +import { AsyncTaskInfoService } from '@cloudbeaver/core-root'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; import { isObjectsEqual } from '@cloudbeaver/core-utils'; import { DatabaseDataAccessMode, DatabaseDataModel, DataViewerSettingsService, - IDatabaseDataModel, - IDatabaseResultSet, + type IDatabaseDataModel, + ResultSetDataSource, TableViewerStorageService, } from '@cloudbeaver/plugin-data-viewer'; -import { GroupingDataSource, IDataGroupingOptions } from './GroupingDataSource'; -import type { IGroupingQueryState } from './IGroupingQueryState'; +import { GroupingDataSource } from './GroupingDataSource.js'; +import type { IDVResultSetGroupingPresentationState } from './IDVResultSetGroupingPresentationState.js'; +import type { IGroupingQueryState } from './IGroupingQueryState.js'; export interface IGroupingDataModel { - model: IDatabaseDataModel; + model: IDatabaseDataModel; } export function useGroupingDataModel( - sourceModel: IDatabaseDataModel, + sourceModel: IDatabaseDataModel, sourceResultIndex: number, - state: IGroupingQueryState, + state: IGroupingQueryState & IDVResultSetGroupingPresentationState, ): IGroupingDataModel { const tableViewerStorageService = useService(TableViewerStorageService); - const app = useService(App); + const serviceProvider = useService(IServiceProvider); const graphQLService = useService(GraphQLService); const asyncTaskInfoService = useService(AsyncTaskInfoService); const dataViewerSettingsService = useService(DataViewerSettingsService); @@ -49,9 +51,22 @@ export function useGroupingDataModel( const model = useObjectRef( () => { - const source = new GroupingDataSource(app.getServiceInjector(), graphQLService, asyncTaskInfoService); + if (tableViewerStorageService.has(state.modelId)) { + const model = tableViewerStorageService.get(state.modelId) as IDatabaseDataModel; + return { + source: model.source, + model, + dispose() { + this.model.dispose(); + tableViewerStorageService.remove(state.modelId); + }, + }; + } + const source = new GroupingDataSource(serviceProvider, graphQLService, asyncTaskInfoService); + source.setKeepExecutionContextOnDispose(true); const model = tableViewerStorageService.add(new DatabaseDataModel(source)); + state.modelId = model.id; model.setAccess(DatabaseDataAccessMode.Readonly).setCountGain(dataViewerSettingsService.getDefaultRowsCount()).setSlice(0); @@ -68,6 +83,13 @@ export function useGroupingDataModel( ['dispose'], ); + const prevStateRef = useRef({ + columns: state.columns, + functions: state.functions, + showDuplicatesOnly: state.showDuplicatesOnly, + sourceResultId: sourceModel.source.getResult(sourceResultIndex)?.id, + }); + useEffect(() => { sourceModel.onDispose.addHandler(model.dispose); return () => { @@ -78,7 +100,7 @@ export function useGroupingDataModel( useEffect(() => { const sub = reaction( () => { - const result = sourceModel.source.hasResult(sourceResultIndex) ? sourceModel.source.getResult(sourceResultIndex) : null; + const result = sourceModel.source.getResult(sourceResultIndex); return { columns: state.columns, @@ -87,17 +109,32 @@ export function useGroupingDataModel( sourceResultId: result?.id, }; }, - async ({ columns, functions, sourceResultId }) => { + async ({ columns, functions, sourceResultId, showDuplicatesOnly }) => { + const prevState = prevStateRef.current; + + if ( + columns == prevState.columns && + functions == prevState.functions && + sourceResultId == prevState.sourceResultId && + showDuplicatesOnly == prevState.showDuplicatesOnly + ) { + return; + } + + prevStateRef.current = { columns, functions, sourceResultId, showDuplicatesOnly }; + if (columns.length !== 0 && functions.length !== 0 && sourceResultId) { const executionContext = sourceModel.source.executionContext; - model.model.source.setExecutionContext(executionContext).setSupportedDataFormats(connectionInfo?.supportedDataFormats ?? []); + model.source.setExecutionContext(executionContext).setSupportedDataFormats(connectionInfo?.supportedDataFormats ?? []); const context = executionContext?.context; if (context) { const connectionKey = createConnectionParam(context.projectId, context.connectionId); model.model - .setOptions({ + .setCountGain(dataViewerSettingsService.getDefaultRowsCount()) + .setSlice(0) + .source.setOptions({ query: '', columns, functions, @@ -107,9 +144,7 @@ export function useGroupingDataModel( constraints: [], whereFilter: '', }) - .setCountGain(dataViewerSettingsService.getDefaultRowsCount()) - .setSlice(0) - .source.resetData(); + .resetData(); } } else { model.model @@ -127,9 +162,5 @@ export function useGroupingDataModel( return sub; }, [state, sourceModel, sourceResultIndex]); - useEffect(() => model.dispose, []); - - return { - model: model.model, - }; + return model; } diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDnDColumns.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDnDColumns.ts index ca30042d3d..6153b40b16 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDnDColumns.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/useGroupingDnDColumns.ts @@ -1,23 +1,24 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { IDNDBox, useDNDBox } from '@cloudbeaver/core-ui'; +import { type IDNDBox, useDNDBox } from '@cloudbeaver/core-ui'; import { DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY, - IDatabaseDataModel, - IDatabaseResultSet, - IResultSetColumnKey, + type IDatabaseDataModel, + type IResultSetColumnKey, + isResultSetDataSource, ResultSetDataAction, + ResultSetDataSource, } from '@cloudbeaver/plugin-data-viewer'; -import type { IGroupingQueryState } from './IGroupingQueryState'; -import type { IGroupingDataModel } from './useGroupingDataModel'; +import type { IGroupingQueryState } from './IGroupingQueryState.js'; +import type { IGroupingDataModel } from './useGroupingDataModel.js'; interface IGroupingQueryResult { dndBox: IDNDBox; @@ -26,20 +27,15 @@ interface IGroupingQueryResult { export function useGroupingDnDColumns( state: IGroupingQueryState, - sourceModel: IDatabaseDataModel, + sourceModel: IDatabaseDataModel, groupingModel: IGroupingDataModel, ): IGroupingQueryResult { - async function dropItem( - model: IDatabaseDataModel, - resultIndex: number, - columnKey: IResultSetColumnKey | null, - outside: boolean, - ) { + async function dropItem(source: ResultSetDataSource, resultIndex: number, columnKey: IResultSetColumnKey | null, outside: boolean) { if (!columnKey) { return; } - const resultSetDataAction = model.source.getAction(resultIndex, ResultSetDataAction); + const resultSetDataAction = source.getAction(resultIndex, ResultSetDataAction); const name = resultSetDataAction.getColumn(columnKey)?.name; if (!name) { @@ -59,31 +55,31 @@ export function useGroupingDnDColumns( const dndBox = useDNDBox({ canDrop: context => { - const model = context.tryGet(DATA_CONTEXT_DV_DDM); + const model = context.get(DATA_CONTEXT_DV_DDM)!; - return context.has(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY) && model === sourceModel; + return isResultSetDataSource(model.source) && context.has(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY) && model === sourceModel; }, onDrop: async context => { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); - const columnKey = context.get(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY); + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const columnKey = context.get(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY)!; - dropItem(model, resultIndex, columnKey, false); + dropItem(model.source as unknown as ResultSetDataSource, resultIndex, columnKey, false); }, }); const dndThrowBox = useDNDBox({ canDrop: context => { - const model = context.tryGet(DATA_CONTEXT_DV_DDM); + const model = context.get(DATA_CONTEXT_DV_DDM); - return context.has(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY) && model?.id === groupingModel.model.id; + return context.has(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY) && model?.id === groupingModel.model.id && isResultSetDataSource(model.source); }, onDrop: async context => { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); - const columnKey = context.get(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY); + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const columnKey = context.get(DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY)!; - dropItem(model, resultIndex, columnKey, true); + dropItem(model.source as unknown as ResultSetDataSource, resultIndex, columnKey, true); }, }); diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/tsconfig.json b/webapp/packages/plugin-data-viewer-result-set-grouping/tsconfig.json index 53a7e95e1a..35c15896ac 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/tsconfig.json +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/tsconfig.json @@ -1,46 +1,53 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-data-viewer/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../plugin-sql-editor/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-connections" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../core-data-context/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-sdk" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-utils" }, { - "path": "../core-view/tsconfig.json" + "path": "../core-view" + }, + { + "path": "../plugin-data-viewer" + }, + { + "path": "../plugin-sql-editor" } ], "include": [ @@ -52,7 +59,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/.gitignore b/webapp/packages/plugin-data-viewer-result-trace-details/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/package.json b/webapp/packages/plugin-data-viewer-result-trace-details/package.json new file mode 100644 index 0000000000..37b9bf969a --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/package.json @@ -0,0 +1,44 @@ +{ + "name": "@cloudbeaver/plugin-data-viewer-result-trace-details", + "type": "module", + "sideEffects": [ + "./lib/module.js", + "./lib/index.js", + "src/**/*.css", + "src/**/*.scss", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "exports": { + ".": "./lib/index.js" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf --glob lib", + "lint": "eslint ./src/ --ext .ts,.tsx", + "validate-dependencies": "core-cli-validate-dependencies" + }, + "dependencies": { + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/plugin-data-grid": "workspace:*", + "@cloudbeaver/plugin-data-viewer": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" + }, + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } +} diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/public/icons/result_details.svg b/webapp/packages/plugin-data-viewer-result-trace-details/public/icons/result_details.svg new file mode 100644 index 0000000000..0c8019bbcb --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/public/icons/result_details.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/public/icons/result_details_m.svg b/webapp/packages/plugin-data-viewer-result-trace-details/public/icons/result_details_m.svg new file mode 100644 index 0000000000..43545d7e13 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/public/icons/result_details_m.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/public/icons/result_details_sm.svg b/webapp/packages/plugin-data-viewer-result-trace-details/public/icons/result_details_sm.svg new file mode 100644 index 0000000000..986c1a6014 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/public/icons/result_details_sm.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsBootstrap.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsBootstrap.ts new file mode 100644 index 0000000000..16916767bc --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsBootstrap.ts @@ -0,0 +1,37 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { DataPresentationService, DataPresentationType } from '@cloudbeaver/plugin-data-viewer'; + +const DVResultTraceDetailsPresentation = importLazyComponent(() => + import('./DVResultTraceDetailsPresentation.js').then(module => module.DVResultTraceDetailsPresentation), +); + +@injectable(() => [DataPresentationService]) +export class DVResultTraceDetailsBootstrap extends Bootstrap { + constructor(private readonly dataPresentationService: DataPresentationService) { + super(); + } + + override register() { + this.dataPresentationService.add({ + id: 'result-trace-details-presentation', + type: DataPresentationType.toolsPanel, + dataFormat: ResultDataFormat.Resultset, + icon: '/icons/result_details_sm.svg', + title: 'plugin_data_viewer_result_trace_details', + hidden(dataFormat, model, resultIndex) { + const result = model.source.getResult(resultIndex); + return !result?.data?.hasDynamicTrace; + }, + getPresentationComponent: () => DVResultTraceDetailsPresentation, + }); + } +} diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsPresentation.module.css b/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsPresentation.module.css new file mode 100644 index 0000000000..5ef47b9de6 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsPresentation.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.container { + composes: theme-typography--caption from global; + height: 100%; +} diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsPresentation.tsx b/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsPresentation.tsx new file mode 100644 index 0000000000..d504f90e47 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsPresentation.tsx @@ -0,0 +1,80 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { reaction } from 'mobx'; +import { observer } from 'mobx-react-lite'; + +import { s, TextPlaceholder, useAutoLoad, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { DataGrid, useCreateGridReactiveValue } from '@cloudbeaver/plugin-data-grid'; +import { type DataPresentationComponent, type IDatabaseDataOptions, isResultSetDataModel } from '@cloudbeaver/plugin-data-viewer'; + +import classes from './DVResultTraceDetailsPresentation.module.css'; +import { useResultTraceDetails } from './useResultTraceDetails.js'; + +export const DVResultTraceDetailsPresentation: DataPresentationComponent = observer(function DVResultTraceDetailsPresentation({ + model, + resultIndex, +}) { + if (!isResultSetDataModel(model)) { + throw new Error('DVResultTraceDetailsPresentation can only be used with ResultSetDataSource'); + } + const translate = useTranslate(); + const styles = useS(classes); + const state = useResultTraceDetails(model, resultIndex); + + useAutoLoad(DVResultTraceDetailsPresentation, state, undefined, undefined, true); + const trace = state.trace; + + const columnsCount = useCreateGridReactiveValue(() => 3, null, []); + const rowCount = useCreateGridReactiveValue( + () => trace?.length || 0, + onValueChange => reaction(() => trace?.length || 0, onValueChange), + [trace], + ); + + function getCell(rowIdx: number, colIdx: number) { + switch (colIdx) { + case 0: + return trace![rowIdx]?.name ?? ''; + case 1: + return trace![rowIdx]?.value ?? ''; + case 2: + return trace![rowIdx]?.description ?? ''; + } + + return ''; + } + + const cell = useCreateGridReactiveValue(getCell, (onValueChange, rowIdx, colIdx) => reaction(() => getCell(rowIdx, colIdx), onValueChange), [ + trace, + ]); + + function getHeaderText(colIdx: number) { + switch (colIdx) { + case 0: + return translate('ui_name'); + case 1: + return translate('ui_value'); + case 2: + return translate('ui_description'); + } + + return ''; + } + + const headerText = useCreateGridReactiveValue(getHeaderText, (onValueChange, colIdx) => reaction(() => getHeaderText(colIdx), onValueChange), []); + + if (!trace?.length) { + return {translate('plugin_data_viewer_result_trace_no_data_placeholder')}; + } + + return ( +
+ 30} rowCount={rowCount} /> +
+ ); +}); diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsService.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsService.ts new file mode 100644 index 0000000000..8396f5c111 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsService.ts @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; + +@injectable(() => [GraphQLService]) +export class DVResultTraceDetailsService { + constructor(private readonly graphQLService: GraphQLService) {} + + async getTraceDetails(projectId: string, connectionId: string, contextId: string, resultsId: string) { + const trace = await this.graphQLService.sdk.getSqlDynamicTrace({ + projectId, + connectionId, + contextId, + resultsId, + }); + + return trace; + } +} diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/LocaleService.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/LocaleService.ts new file mode 100644 index 0000000000..6d1a499028 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/LocaleService.ts @@ -0,0 +1,37 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable(() => [LocalizationService]) +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + override register(): void { + this.localizationService.addProvider(this.provider.bind(this)); + } + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru.js')).default; + case 'it': + return (await import('./locales/it.js')).default; + case 'zh': + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; + default: + return (await import('./locales/en.js')).default; + } + } +} diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/index.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/index.ts new file mode 100644 index 0000000000..f7f42ea2a6 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/index.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { dataViewerResultTraceDetailsPlugin } from './manifest.js'; + +export default dataViewerResultTraceDetailsPlugin; +export { dataViewerResultTraceDetailsPlugin }; diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/en.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/en.ts new file mode 100644 index 0000000000..4d91777a6a --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/en.ts @@ -0,0 +1,4 @@ +export default [ + ['plugin_data_viewer_result_trace_details', 'Result details'], + ['plugin_data_viewer_result_trace_no_data_placeholder', 'No trace details were found'], +]; diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/fr.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/fr.ts new file mode 100644 index 0000000000..4d91777a6a --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/fr.ts @@ -0,0 +1,4 @@ +export default [ + ['plugin_data_viewer_result_trace_details', 'Result details'], + ['plugin_data_viewer_result_trace_no_data_placeholder', 'No trace details were found'], +]; diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/it.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/it.ts new file mode 100644 index 0000000000..4d91777a6a --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/it.ts @@ -0,0 +1,4 @@ +export default [ + ['plugin_data_viewer_result_trace_details', 'Result details'], + ['plugin_data_viewer_result_trace_no_data_placeholder', 'No trace details were found'], +]; diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/ru.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/ru.ts new file mode 100644 index 0000000000..15e16eca77 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/ru.ts @@ -0,0 +1,4 @@ +export default [ + ['plugin_data_viewer_result_trace_details', 'Детализация результата'], + ['plugin_data_viewer_result_trace_no_data_placeholder', 'Детали результата не найдены'], +]; diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/vi.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/vi.ts new file mode 100644 index 0000000000..c39897e124 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/vi.ts @@ -0,0 +1,4 @@ +export default [ + ['plugin_data_viewer_result_trace_details', 'Chi tiết kết quả'], + ['plugin_data_viewer_result_trace_no_data_placeholder', 'Không tìm thấy chi tiết theo dõi'], +]; diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/zh.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/zh.ts new file mode 100644 index 0000000000..5bae4ca415 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/locales/zh.ts @@ -0,0 +1,4 @@ +export default [ + ['plugin_data_viewer_result_trace_details', '结果明细'], + ['plugin_data_viewer_result_trace_no_data_placeholder', '未找到跟踪详细信息'], +]; diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/manifest.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/manifest.ts new file mode 100644 index 0000000000..3cc4472b8b --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/manifest.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; + +export const dataViewerResultTraceDetailsPlugin: PluginManifest = { + info: { + name: 'Result trace details Data Editor plugin', + }, +}; diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/module.ts b/webapp/packages/plugin-data-viewer-result-trace-details/src/module.ts new file mode 100644 index 0000000000..739d017fdb --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/module.ts @@ -0,0 +1,24 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { LocaleService } from './LocaleService.js'; +import { DVResultTraceDetailsService } from './DVResultTraceDetailsService.js'; +import { DVResultTraceDetailsBootstrap } from './DVResultTraceDetailsBootstrap.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-data-viewer-result-trace-details', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, proxy(DVResultTraceDetailsBootstrap)) + .addSingleton(DVResultTraceDetailsBootstrap) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(DVResultTraceDetailsService); + }, +}); diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/src/useResultTraceDetails.tsx b/webapp/packages/plugin-data-viewer-result-trace-details/src/useResultTraceDetails.tsx new file mode 100644 index 0000000000..2d9fa0bca4 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/src/useResultTraceDetails.tsx @@ -0,0 +1,117 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { computed, observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import type { DynamicTraceProperty, GetSqlDynamicTraceMutation } from '@cloudbeaver/core-sdk'; +import { type ILoadableState, isContainsException } from '@cloudbeaver/core-utils'; +import { + DatabaseMetadataAction, + type IDatabaseDataModel, + type IResultSetElementKey, + ResultSetCacheAction, + ResultSetDataSource, +} from '@cloudbeaver/plugin-data-viewer'; + +import { DVResultTraceDetailsService } from './DVResultTraceDetailsService.js'; + +type ResultTraceDetailsPromise = Promise; + +interface MetadataState { + promise: ResultTraceDetailsPromise | null; + exception: Error | null; +} +interface State extends ILoadableState { + readonly trace: DynamicTraceProperty[] | undefined; + model: IDatabaseDataModel; + resultIndex: number; + cache: ResultSetCacheAction; + metadataState: MetadataState; +} + +const RESULT_TRACE_DETAILS_CACHE_KEY = Symbol('@cache/ResultTraceDetails'); +const RESULT_TRACE_DETAILS_METADATA_KEY = 'result-trace-details-panel'; +// @TODO Probably we want to implement a cache behavior that will only use Scope Key as sometimes we want +// a cache that only exists as long as result exists but dont want to specify row/column indexes +const FAKE_ELEMENT_KEY: IResultSetElementKey = { + column: { index: Number.MAX_SAFE_INTEGER }, + row: { index: Number.MAX_SAFE_INTEGER, subIndex: Number.MAX_SAFE_INTEGER }, +}; + +export function useResultTraceDetails(model: IDatabaseDataModel, resultIndex: number) { + const dvResultTraceDetailsService = useService(DVResultTraceDetailsService); + const cache = model.source.getAction(resultIndex, ResultSetCacheAction); + const metadataAction = model.source.getAction(resultIndex, DatabaseMetadataAction); + + const metadataState = metadataAction.get(RESULT_TRACE_DETAILS_METADATA_KEY, () => + observable({ + promise: null, + exception: null, + }), + ); + + const state = useObservableRef( + () => ({ + get trace(): DynamicTraceProperty[] | undefined { + return this.cache.get(FAKE_ELEMENT_KEY, RESULT_TRACE_DETAILS_CACHE_KEY); + }, + get promise(): ResultTraceDetailsPromise | null { + return this.metadataState.promise; + }, + get exception(): Error | null { + return this.metadataState.exception; + }, + isError() { + return isContainsException(this.exception); + }, + isLoaded() { + return this.trace !== undefined; + }, + async load() { + const result = this.model.source.getResult(this.resultIndex); + + try { + if (!result?.id) { + throw new Error('Result is not found'); + } + + if (this.metadataState.promise) { + return; + } + + this.metadataState.exception = null; + + this.metadataState.promise = dvResultTraceDetailsService.getTraceDetails( + result.projectId, + result.connectionId, + result.contextId, + result.id, + ); + + const { trace } = await this.metadataState.promise; + + this.cache.set(FAKE_ELEMENT_KEY, RESULT_TRACE_DETAILS_CACHE_KEY, trace); + } catch (exception: any) { + this.metadataState.exception = exception; + } finally { + this.metadataState.promise = null; + } + }, + }), + { + promise: computed, + exception: computed, + trace: computed, + model: observable.ref, + }, + { model, resultIndex, cache, metadataState }, + ); + + return state; +} diff --git a/webapp/packages/plugin-data-viewer-result-trace-details/tsconfig.json b/webapp/packages/plugin-data-viewer-result-trace-details/tsconfig.json new file mode 100644 index 0000000000..e046136d79 --- /dev/null +++ b/webapp/packages/plugin-data-viewer-result-trace-details/tsconfig.json @@ -0,0 +1,46 @@ +{ + "extends": "@cloudbeaver/tsconfig/tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true + }, + "references": [ + { + "path": "../core-blocks" + }, + { + "path": "../core-cli" + }, + { + "path": "../core-di" + }, + { + "path": "../core-localization" + }, + { + "path": "../core-sdk" + }, + { + "path": "../core-utils" + }, + { + "path": "../plugin-data-grid" + }, + { + "path": "../plugin-data-viewer" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*" + ] +} diff --git a/webapp/packages/plugin-data-viewer/package.json b/webapp/packages/plugin-data-viewer/package.json index 1f7f76c41f..c3acb00e19 100644 --- a/webapp/packages/plugin-data-viewer/package.json +++ b/webapp/packages/plugin-data-viewer/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-data-viewer", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,48 +11,59 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "pretest": "tsc -b", - "test": "core-cli-test", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "test": "dbeaver-test", + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-codemirror6": "~0.1.0", - "@cloudbeaver/plugin-navigation-tabs": "~0.1.0", - "@cloudbeaver/plugin-object-viewer": "~0.1.0", - "@cloudbeaver/core-authentication": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-data-context": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x", - "@testing-library/jest-dom": "~6.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-browser": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-links": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-navigation-tree": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-settings": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-codemirror6": "workspace:*", + "@cloudbeaver/plugin-datasource-context-switch": "workspace:*", + "@cloudbeaver/plugin-navigation-tabs": "workspace:*", + "@cloudbeaver/plugin-object-viewer": "workspace:*", + "@cloudbeaver/plugin-sql-editor": "workspace:*", + "@cloudbeaver/plugin-sql-editor-navigation-tab": "workspace:*", + "@dbeaver/js-helpers": "workspace:^", + "@dbeaver/result-set-api": "workspace:^", + "lz-string": "1.5.0", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "reflect-metadata": "^0", + "tslib": "^2" }, "devDependencies": { - "@cloudbeaver/tests-runner": "~0.1.0" + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@dbeaver/cli": "workspace:*", + "@dbeaver/react-tests": "workspace:^", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5", + "vitest": "^3" } } diff --git a/webapp/packages/plugin-data-viewer/public/icons/data_row_count.svg b/webapp/packages/plugin-data-viewer/public/icons/data_row_count.svg new file mode 100644 index 0000000000..3b76642345 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/public/icons/data_row_count.svg @@ -0,0 +1,21 @@ + + + + compile + Created with Sketch. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_no_wrap_lines.svg b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_no_wrap_lines.svg new file mode 100644 index 0000000000..a77c6a1842 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_no_wrap_lines.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_no_wrap_lines_m.svg b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_no_wrap_lines_m.svg new file mode 100644 index 0000000000..2ec1cb24fc --- /dev/null +++ b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_no_wrap_lines_m.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_no_wrap_lines_sm.svg b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_no_wrap_lines_sm.svg new file mode 100644 index 0000000000..4fcbcc1ca0 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_no_wrap_lines_sm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_wrap_lines.svg b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_wrap_lines.svg new file mode 100644 index 0000000000..41d9b8da64 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_wrap_lines.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_wrap_lines_m.svg b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_wrap_lines_m.svg new file mode 100644 index 0000000000..9e2189fbd0 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_wrap_lines_m.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_wrap_lines_sm.svg b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_wrap_lines_sm.svg new file mode 100644 index 0000000000..cfa247bac6 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/public/icons/plugin_data_viewer_wrap_lines_sm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 04adc6951a..18c5ae45f4 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,45 +8,47 @@ import { computed, makeObservable, observable } from 'mobx'; import type { ConnectionExecutionContextService, IConnectionExecutionContext, IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; -import type { IServiceInjector } from '@cloudbeaver/core-di'; +import type { IServiceProvider } from '@cloudbeaver/core-di'; import type { ITask } from '@cloudbeaver/core-executor'; +import type { AsyncTask, AsyncTaskInfoService } from '@cloudbeaver/core-root'; import { - AsyncTaskInfoService, GraphQLService, ResultDataFormat, - SqlExecuteInfo, - SqlQueryResults, - UpdateResultsDataBatchMutationVariables, + type SqlExecuteInfo, + type SqlQueryResults, + type AsyncUpdateResultsDataBatchMutationVariables, } from '@cloudbeaver/core-sdk'; +import { uuid } from '@cloudbeaver/core-utils'; -import { DocumentEditAction } from './DatabaseDataModel/Actions/Document/DocumentEditAction'; -import { ResultSetEditAction } from './DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; -import { DatabaseDataSource } from './DatabaseDataModel/DatabaseDataSource'; -import type { IDatabaseDataOptions } from './DatabaseDataModel/IDatabaseDataOptions'; -import type { IDatabaseResultSet } from './DatabaseDataModel/IDatabaseResultSet'; +import { DocumentEditAction } from './DatabaseDataModel/Actions/Document/DocumentEditAction.js'; +import type { IResultSetBlobValue } from './DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue.js'; +import { ResultSetEditAction } from './DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.js'; +import type { IDatabaseDataOptions } from './DatabaseDataModel/IDatabaseDataOptions.js'; +import type { IDatabaseResultSet } from './DatabaseDataModel/IDatabaseResultSet.js'; +import { ResultSetDataSource } from './ResultSet/ResultSetDataSource.js'; export interface IDataContainerOptions extends IDatabaseDataOptions { containerNodePath: string; } -export class ContainerDataSource extends DatabaseDataSource { +export class ContainerDataSource extends ResultSetDataSource { currentTask: ITask | null; - get canCancel(): boolean { + override get canCancel(): boolean { return this.currentTask?.cancellable || false; } - get cancelled(): boolean { + override get cancelled(): boolean { return this.currentTask?.cancelled || false; } constructor( - readonly serviceInjector: IServiceInjector, - private readonly graphQLService: GraphQLService, - private readonly asyncTaskInfoService: AsyncTaskInfoService, - private readonly connectionExecutionContextService: ConnectionExecutionContextService, + serviceProvider: IServiceProvider, + graphQLService: GraphQLService, + asyncTaskInfoService: AsyncTaskInfoService, + protected connectionExecutionContextService: ConnectionExecutionContextService, ) { - super(serviceInjector); + super(serviceProvider, graphQLService, asyncTaskInfoService); this.currentTask = null; this.executionContext = null; @@ -57,60 +59,20 @@ export class ContainerDataSource extends DatabaseDataSource { - if (this.currentTask) { - await this.currentTask.cancel(); - } + override async cancel(): Promise { + await super.cancel(); + await this.currentTask?.cancel(); } async request(prevResults: IDatabaseResultSet[]): Promise { - const options = this.options; - - if (!options) { - throw new Error('containerNodePath must be provided for table'); - } - const executionContext = await this.ensureContextCreated(); const context = executionContext.context!; - const offset = this.offset; const limit = this.count; - - let firstResultId: string | undefined; - - if ( - prevResults.length === 1 && - prevResults[0].contextId === context.id && - prevResults[0].connectionId === context.connectionId && - prevResults[0].id !== null - ) { - firstResultId = prevResults[0].id; - } - - const task = this.asyncTaskInfoService.create(async () => { - const { taskInfo } = await this.graphQLService.sdk.asyncReadDataFromContainer({ - connectionId: context.connectionId, - contextId: context.id, - containerNodePath: options.containerNodePath, - resultId: firstResultId, - filter: { - offset, - limit, - constraints: options.constraints, - where: options.whereFilter || undefined, - }, - dataFormat: this.dataFormat, - }); - - return taskInfo; - }); + const task = await this.getRequestTask(prevResults, context); this.currentTask = executionContext.run( async () => { @@ -136,8 +98,6 @@ export class ContainerDataSource extends DatabaseDataSource { + const { taskInfo } = await this.graphQLService.sdk.asyncUpdateResultsDataBatch(updateVariables); + return taskInfo; + }); - this.requestInfo = { - ...this.requestInfo, - requestDuration: response.result.duration, - requestMessage: 'Saved successfully', - source: null, - }; + this.currentTask = executionContext.run( + async () => { + const info = await this.asyncTaskInfoService.run(task); + const { result } = await this.graphQLService.sdk.getSqlExecuteTaskResults({ taskId: info.id }); + + return result; + }, + () => this.asyncTaskInfoService.cancel(task.id), + () => this.asyncTaskInfoService.remove(task.id), + ); + + const response = await this.currentTask; if (editor) { - const responseResult = this.transformResults(executionContextInfo, response.result.results, 0).find( - newResult => newResult.id === result.id, - ); + const responseResult = this.transformResults(executionContextInfo, response.results, 0).find(newResult => newResult.id === result.id); if (responseResult) { editor.applyUpdate(responseResult); } } + + this.requestInfo = { + ...this.requestInfo, + requestDuration: response.duration, + requestMessage: 'plugin_data_viewer_result_set_save_success', + source: null, + }; } + this.clearError(); } catch (exception: any) { this.error = exception; @@ -198,43 +191,65 @@ export class ContainerDataSource extends DatabaseDataSource { - await this.closeResults(this.results); - await this.executionContext?.destroy(); + protected getConfig(prevResults: IDatabaseResultSet[], context: IConnectionExecutionContextInfo) { + const options = this.options; + + if (!options) { + throw new Error('Options must be provided'); + } + + const offset = this.offset; + const limit = this.count; + const resultId = this.getPreviousResultId(prevResults, context); + + return { + projectId: context.projectId, + connectionId: context.connectionId, + contextId: context.id, + containerNodePath: options.containerNodePath, + resultId, + filter: { + offset, + limit, + constraints: options.constraints, + where: options.whereFilter || undefined, + }, + dataFormat: this.dataFormat, + }; } - private async closeResults(results: IDatabaseResultSet[]) { - await this.connectionExecutionContextService.load(); + protected async getRequestTask(prevResults: IDatabaseResultSet[], context: IConnectionExecutionContextInfo): Promise { + const task = this.asyncTaskInfoService.create(async () => { + const config = this.getConfig(prevResults, context); + const { taskInfo } = await this.graphQLService.sdk.asyncReadDataFromContainer(config); + return taskInfo; + }); - if (!this.executionContext?.context) { - return; - } + return task; + } - for (const result of results) { - if (result.id === null) { - continue; - } - try { - await this.graphQLService.sdk.closeResult({ - connectionId: result.connectionId, - contextId: result.contextId, - resultId: result.id, - }); - } catch (exception: any) { - console.log(`Error closing result (${result.id}):`, exception); - } + override setExecutionContext(context: IConnectionExecutionContext | null): this { + super.setExecutionContext(context); + + for (const result of this.results) { + result.id = null; } + + return this; } private transformResults(executionContextInfo: IConnectionExecutionContextInfo, results: SqlQueryResults[], limit: number): IDatabaseResultSet[] { return results.map((result, index) => ({ id: result.resultSet?.id || '0', uniqueResultId: `${executionContextInfo.connectionId}_${executionContextInfo.id}_${index}`, + projectId: executionContextInfo.projectId, connectionId: executionContextInfo.connectionId, contextId: executionContextInfo.id, dataFormat: result.dataFormat!, updateRowCount: result.updateRowCount || 0, - loadedFully: (result.resultSet?.rows?.length || 0) < limit, + loadedFully: (result.resultSet?.rowsWithMetaData?.length || 0) < limit, + count: result.resultSet?.rowsWithMetaData?.length || 0, + totalCount: null, data: result.resultSet, })); } @@ -255,6 +270,7 @@ export class ContainerDataSource extends DatabaseDataSource { +export interface IDataPresentationProps extends HTMLAttributes { dataFormat: ResultDataFormat; - model: IDatabaseDataModel; + model: IDatabaseDataModel; actions: IDataTableActions; resultIndex: number; simple: boolean; @@ -27,9 +28,7 @@ export enum DataPresentationType { toolsPanel, } -export type DataPresentationComponent = React.FunctionComponent< - IDataPresentationProps ->; +export type DataPresentationComponent = React.FunctionComponent; export type PresentationTabProps = TabProps & { presentation: IDataPresentationOptions; @@ -44,6 +43,7 @@ export interface IDataPresentationOptions { type?: DataPresentationType; title?: string; icon?: string; + order?: number; hidden?: (dataFormat: ResultDataFormat | null, model: IDatabaseDataModel, resultIndex: number) => boolean; getPresentationComponent: () => DataPresentationComponent; getTabComponent?: () => PresentationTabComponent; @@ -52,10 +52,14 @@ export interface IDataPresentationOptions { export interface IDataPresentation extends IDataPresentationOptions { type: DataPresentationType; + order: number; } @injectable() export class DataPresentationService { + private get orderedPresentations(): IDataPresentation[] { + return Array.from(this.dataPresentations.values()).sort((a, b) => b.order - a.order); + } private readonly dataPresentations: Map; constructor() { @@ -73,7 +77,7 @@ export class DataPresentationService { model: IDatabaseDataModel, resultIndex: number, ): IDataPresentation[] { - return Array.from(this.dataPresentations.values()).filter(presentation => { + return this.orderedPresentations.filter(presentation => { if (presentation.dataFormat !== undefined && !supportedDataFormats.includes(presentation.dataFormat)) { return false; } @@ -104,7 +108,7 @@ export class DataPresentationService { } } - for (const presentation of this.dataPresentations.values()) { + for (const presentation of this.orderedPresentations) { if ( (presentation.dataFormat === undefined || presentation.dataFormat === dataFormat) && presentation.type === type && @@ -121,6 +125,7 @@ export class DataPresentationService { this.dataPresentations.set(options.id, { ...options, type: options.type || DataPresentationType.main, + order: options.order || Number.MAX_SAFE_INTEGER, }); } } diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerBootstrap.ts b/webapp/packages/plugin-data-viewer/src/DataViewerBootstrap.ts index 725f34787b..983e8b5b68 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerBootstrap.ts @@ -1,24 +1,30 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { DataViewerTabService } from './DataViewerTabService'; +import { DataViewerTabService } from './DataViewerTabService.js'; +import { ResultSetTableFooterMenuService } from './ResultSet/ResultSetTableFooterMenuService.js'; +import { TableFooterMenuService } from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.js'; -@injectable() +@injectable(() => [DataViewerTabService, TableFooterMenuService, ResultSetTableFooterMenuService]) export class DataViewerBootstrap extends Bootstrap { - constructor(private readonly dataViewerTabService: DataViewerTabService) { + constructor( + private readonly dataViewerTabService: DataViewerTabService, + private readonly tableFooterMenuService: TableFooterMenuService, + private readonly resultSetTableFooterMenuService: ResultSetTableFooterMenuService, + ) { super(); } - register(): void | Promise { + override register(): void | Promise { this.dataViewerTabService.registerTabHandler(); this.dataViewerTabService.register(); + this.tableFooterMenuService.register(); + this.resultSetTableFooterMenuService.register(); } - - load(): void {} } diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerDataChangeConfirmationService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerDataChangeConfirmationService.ts index d553a9d841..be4fa89f2e 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerDataChangeConfirmationService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerDataChangeConfirmationService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,13 +9,14 @@ import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { executorHandlerFilter, ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { DatabaseEditAction } from './DatabaseDataModel/Actions/DatabaseEditAction'; -import type { IRequestEventData } from './DatabaseDataModel/IDatabaseDataModel'; -import { TableViewerStorageService } from './TableViewer/TableViewerStorageService'; +import { DatabaseEditAction } from './DatabaseDataModel/Actions/DatabaseEditAction.js'; +import type { IRequestEventData } from './DatabaseDataModel/IDatabaseDataModel.js'; +import { DatabaseDataSourceOperation } from './DatabaseDataModel/IDatabaseDataSource.js'; +import { TableViewerStorageService } from './TableViewer/TableViewerStorageService.js'; -@injectable() +@injectable(() => [CommonDialogService, TableViewerStorageService, NotificationService]) export class DataViewerDataChangeConfirmationService { constructor( private readonly commonDialogService: CommonDialogService, @@ -25,29 +26,30 @@ export class DataViewerDataChangeConfirmationService { this.checkUnsavedData = this.checkUnsavedData.bind(this); } + // TODO: should be automatically called when the model is created, we can add executor to TableViewerStorageService for that trackTableDataUpdate(modelId: string) { const model = this.dataViewerTableService.get(modelId); if (model && !model.onRequest.hasHandler(this.checkUnsavedData)) { - model.onRequest.addHandler(this.checkUnsavedData); + model.onRequest.addHandler(executorHandlerFilter(({ operation }) => operation === DatabaseDataSourceOperation.Request, this.checkUnsavedData)); } } - private async checkUnsavedData({ type, model }: IRequestEventData, contexts: IExecutionContextProvider>) { - if (type === 'before') { + private async checkUnsavedData({ stage, model }: IRequestEventData, contexts: IExecutionContextProvider) { + if (stage === 'request') { const confirmationContext = contexts.getContext(SaveConfirmedContext); if (confirmationContext.confirmed === false) { return; } - const results = model.getResults(); + const results = model.source.getResults(); try { for (let resultIndex = 0; resultIndex < results.length; resultIndex++) { const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); - if (editor?.isEdited() && model.source.executionContext?.context) { + if (editor?.isEdited() && !model.isDisabled(resultIndex)) { if (confirmationContext.confirmed) { await model.save(); } else { @@ -70,8 +72,8 @@ export class DataViewerDataChangeConfirmationService { } } } catch (exception: any) { - ExecutorInterrupter.interrupt(contexts); this.notificationService.logException(exception, 'data_viewer_data_save_error_title'); + throw exception; } } } diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerPanel.module.css b/webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerPanel.module.css new file mode 100644 index 0000000000..093b9fc4c6 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerPanel.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tableViewerLoader { + padding: 8px; + padding-bottom: 0; +} diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerPanel.tsx b/webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerPanel.tsx index 3a08117c30..29496f0829 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerPanel.tsx +++ b/webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerPanel.tsx @@ -1,30 +1,24 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; import { useCallback } from 'react'; -import styled, { css } from 'reshadow'; -import { Loader, TextPlaceholder } from '@cloudbeaver/core-blocks'; +import { TextPlaceholder, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks'; import type { ObjectPagePanelComponent } from '@cloudbeaver/plugin-object-viewer'; -import type { IDataViewerPageState } from '../IDataViewerPageState'; -import { TableViewerLoader } from '../TableViewer/TableViewerLoader'; -import { useDataViewerDatabaseDataModel } from './useDataViewerDatabaseDataModel'; - -const styles = css` - TableViewerLoader { - padding: 8px; - padding-bottom: 0; - } -`; +import type { IDataViewerPageState } from '../IDataViewerPageState.js'; +import { TableViewerLoader } from '../TableViewer/TableViewerLoader.js'; +import classes from './DataViewerPanel.module.css'; +import { useDataViewerPanel } from './useDataViewerPanel.js'; export const DataViewerPanel: ObjectPagePanelComponent = observer(function DataViewerPanel({ tab, page }) { - const dataViewerDatabaseDataModel = useDataViewerDatabaseDataModel(tab); + const translate = useTranslate(); + const panel = useDataViewerPanel(tab); const pageState = page.getState(tab); const handlePresentationChange = useCallback( @@ -61,24 +55,21 @@ export const DataViewerPanel: ObjectPagePanelComponent = o [page, tab], ); + useAutoLoad(DataViewerPanel, panel); + if (!tab.handlerState.tableId) { - return Table model not loaded; + return {translate('data_viewer_model_not_loaded')}; } - return styled(styles)( - - {tab.handlerState.tableId ? ( - - ) : ( - Table model not loaded - )} - , + return ( + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerTab.tsx b/webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerTab.tsx index 55a68ec5e8..14b0e08d2a 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerTab.tsx +++ b/webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerTab.tsx @@ -1,35 +1,33 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled from 'reshadow'; -import { Translate, useStyles } from '@cloudbeaver/core-blocks'; +import { Translate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; import { Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; import type { ObjectPageTabComponent } from '@cloudbeaver/plugin-object-viewer'; -import type { IDataViewerPageState } from '../IDataViewerPageState'; +import type { IDataViewerPageState } from '../IDataViewerPageState.js'; -export const DataViewerTab: ObjectPageTabComponent = observer(function DataViewerTab({ tab, page, onSelect, style }) { - const styles = useStyles(style); +export const DataViewerTab: ObjectPageTabComponent = observer(function DataViewerTab({ tab, page, onSelect }) { const navNodeManagerService = useService(NavNodeManagerService); if (!navNodeManagerService.isNodeHasData(tab.handlerState.objectId)) { return null; } - return styled(styles)( - + return ( + - , + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerDatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerDatabaseDataModel.ts deleted file mode 100644 index 43b70bc0d8..0000000000 --- a/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerDatabaseDataModel.ts +++ /dev/null @@ -1,152 +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 { action, computed, observable } from 'mobx'; -import { useEffect } from 'react'; - -import { useObservableRef, useResource } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; -import { ILoadableState, isContainsException } from '@cloudbeaver/core-utils'; -import type { ITab } from '@cloudbeaver/plugin-navigation-tabs'; -import type { IObjectViewerTabState } from '@cloudbeaver/plugin-object-viewer'; - -import { DataPresentationService } from '../DataPresentationService'; -import { DataViewerDataChangeConfirmationService } from '../DataViewerDataChangeConfirmationService'; -import { DataViewerTableService } from '../DataViewerTableService'; -import { DataViewerTabService } from '../DataViewerTabService'; - -export interface IDataViewerDatabaseDataModel extends ILoadableState { - init(): Promise; - load(): Promise; - _exception?: Error[] | Error | null; - _loading: boolean; - tab: ITab; -} - -export function useDataViewerDatabaseDataModel(tab: ITab) { - const dataViewerTabService = useService(DataViewerTabService); - const navNodeManagerService = useService(NavNodeManagerService); - const dataViewerTableService = useService(DataViewerTableService); - const connectionInfoResource = useService(ConnectionInfoResource); - const dataPresentationService = useService(DataPresentationService); - const dataViewerDataChangeConfirmationService = useService(DataViewerDataChangeConfirmationService); - - const connection = useResource(useDataViewerDatabaseDataModel, ConnectionInfoResource, tab.handlerState.connectionKey ?? null); - - const state = useObservableRef( - () => ({ - _exception: null, - _loading: false, - get exception() { - if (isContainsException(connection.exception)) { - return connection.exception; - } - return this._exception; - }, - isLoading(): boolean { - return connection.isLoading() || this._loading; - }, - isLoaded(): boolean { - return connection.isLoaded() && dataViewerTableService.get(this.tab.handlerState.tableId || '') !== undefined; - }, - async reload() { - if (isContainsException(connection.exception)) { - connection.reload(); - } - this.init(); - }, - async load() { - if (isContainsException(this.exception)) { - return; - } - - await this.init(); - }, - async init() { - if (this._loading) { - return; - } - this._loading = true; - try { - if (!this.tab.handlerState.connectionKey) { - this._exception = null; - return; - } - - const node = navNodeManagerService.getNode({ - nodeId: this.tab.handlerState.objectId, - parentId: this.tab.handlerState.parentId, - }); - - if (!navNodeManagerService.isNodeHasData(node)) { - this._exception = null; - return; - } - - let model = dataViewerTableService.get(this.tab.handlerState.tableId || ''); - - if (model && !model.source.executionContext?.context && model.source.results.length > 0) { - model.resetData(); - } - - if (!model) { - await connectionInfoResource.waitLoad(); - const connectionInfo = connectionInfoResource.get(this.tab.handlerState.connectionKey); - - if (!connectionInfo) { - throw new Error("Connection doesn't exists"); - } - - model = dataViewerTableService.create(connectionInfo, node); - this.tab.handlerState.tableId = model.id; - model.source.setOutdated(); - dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id); - - const pageState = dataViewerTabService.page.getState(this.tab); - - if (pageState) { - const presentation = dataPresentationService.get(pageState.presentationId); - - if (presentation?.dataFormat !== undefined) { - model.setDataFormat(presentation.dataFormat); - } - } - } - - if (node?.name) { - model.setName(node.name); - } - this._exception = null; - } catch (exception: any) { - this._exception = exception; - } finally { - this._loading = false; - } - }, - }), - { - exception: computed, - _loading: observable.ref, - _exception: observable.ref, - tab: observable.ref, - isLoaded: action.bound, - isLoading: action.bound, - reload: action.bound, - }, - { - tab, - }, - ); - - useEffect(() => { - state.load(); - }); - - return state; -} diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerPanel.ts b/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerPanel.ts new file mode 100644 index 0000000000..21991950f3 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerPanel.ts @@ -0,0 +1,82 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; +import { useService } from '@cloudbeaver/core-di'; +import { NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; +import type { ITab } from '@cloudbeaver/plugin-navigation-tabs'; +import type { IObjectViewerTabState } from '@cloudbeaver/plugin-object-viewer'; + +import { ContainerDataSource } from '../ContainerDataSource.js'; +import { type IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel.js'; +import { DataPresentationService } from '../DataPresentationService.js'; +import { DataViewerDataChangeConfirmationService } from '../DataViewerDataChangeConfirmationService.js'; +import { DataViewerTableService } from '../DataViewerTableService.js'; +import { DataViewerTabService } from '../DataViewerTabService.js'; +import { TableViewerStorageService } from '../TableViewer/TableViewerStorageService.js'; +import { useDataViewerModel } from '../useDataViewerModel.js'; + +export function useDataViewerPanel(tab: ITab) { + const dataViewerTableService = useService(DataViewerTableService); + const tableViewerStorageService = useService(TableViewerStorageService); + const navNodeManagerService = useService(NavNodeManagerService); + const dataViewerTabService = useService(DataViewerTabService); + const connectionInfoResource = useService(ConnectionInfoResource); + const dataPresentationService = useService(DataPresentationService); + const dataViewerDataChangeConfirmationService = useService(DataViewerDataChangeConfirmationService); + + const model = useDataViewerModel( + tab.handlerState.connectionKey, + async () => { + const node = navNodeManagerService.getNode({ + nodeId: tab.handlerState.objectId, + parentId: tab.handlerState.parentId, + }); + + if (!navNodeManagerService.isNodeHasData(node)) { + return; + } + + let model = tableViewerStorageService.get>(tab.handlerState.tableId || ''); + + if (model && !model.isDisabled() && model.source.results.length > 0) { + model.resetData(); + } + + if (!model) { + await connectionInfoResource.waitLoad(); + const connectionInfo = connectionInfoResource.get(tab.handlerState.connectionKey!); + + if (!connectionInfo) { + throw new Error("Connection doesn't exists"); + } + + model = dataViewerTableService.create(connectionInfo, node); + tab.handlerState.tableId = model.id; + model.source.setOutdated(); + dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id); + + const pageState = dataViewerTabService.page.getState(tab); + + if (pageState) { + const presentation = dataPresentationService.get(pageState.presentationId); + + if (presentation?.dataFormat !== undefined) { + model.setDataFormat(presentation.dataFormat); + } + } + } + + if (node?.name) { + model.setName(node.name); + } + }, + tab.handlerState.tableId, + ); + + return model; +} diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerService.ts index 2de32016b6..3562315164 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerService.ts @@ -1,23 +1,49 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { Connection } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; +import { EAdminPermission, SessionPermissionsResource } from '@cloudbeaver/core-root'; +import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; -import { DataViewerSettingsService } from './DataViewerSettingsService'; +import { DataViewerSettingsService } from './DataViewerSettingsService.js'; +import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel.js'; -@injectable() +export interface IErrorActionsContainerData { + model: IDatabaseDataModel; +} + +@injectable(() => [DataViewerSettingsService, SessionPermissionsResource]) export class DataViewerService { - constructor(private readonly dataViewerSettingsService: DataViewerSettingsService) {} + readonly errorActionsContainer: PlaceholderContainer; + + get canCopyData() { + return this.sessionPermissionsResource.has(EAdminPermission.admin) || !this.dataViewerSettingsService.disableCopyData; + } + + get canExportData() { + return this.sessionPermissionsResource.has(EAdminPermission.admin) || !this.dataViewerSettingsService.disableExportData; + } + + constructor( + private readonly dataViewerSettingsService: DataViewerSettingsService, + private readonly sessionPermissionsResource: SessionPermissionsResource, + ) { + this.errorActionsContainer = new PlaceholderContainer(); + } isDataEditable(connection: Connection) { - const disabled = this.dataViewerSettingsService.settings.isValueDefault('disableEdit') - ? this.dataViewerSettingsService.deprecatedSettings.getValue('disableEdit') - : this.dataViewerSettingsService.settings.getValue('disableEdit'); - return !disabled && !connection.readOnly; + if (connection.readOnly) { + return false; + } + + const isAdmin = this.sessionPermissionsResource.has(EAdminPermission.admin); + const disabled = this.dataViewerSettingsService.disableEdit; + + return isAdmin || !disabled; } } diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts index 9d5f53144d..b3d0e282a6 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts @@ -1,155 +1,146 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 '@testing-library/jest-dom'; - -import { coreAdministrationManifest } from '@cloudbeaver/core-administration'; -import { coreAppManifest } from '@cloudbeaver/core-app'; -import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; -import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; -import { coreBrowserManifest } from '@cloudbeaver/core-browser'; -import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; -import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; -import { coreEventsManifest } from '@cloudbeaver/core-events'; -import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; -import { coreProjectsManifest } from '@cloudbeaver/core-projects'; -import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; -import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; -import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; -import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; -import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; -import { coreRoutingManifest } from '@cloudbeaver/core-routing'; -import { coreSDKManifest } from '@cloudbeaver/core-sdk'; -import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { coreThemingManifest } from '@cloudbeaver/core-theming'; -import { coreUIManifest } from '@cloudbeaver/core-ui'; -import { coreViewManifest } from '@cloudbeaver/core-view'; -import { datasourceContextSwitchPluginManifest } from '@cloudbeaver/plugin-datasource-context-switch'; -import { navigationTabsPlugin } from '@cloudbeaver/plugin-navigation-tabs'; -import { navigationTreePlugin } from '@cloudbeaver/plugin-navigation-tree'; -import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; -import { createApp } from '@cloudbeaver/tests-runner'; - -import { DataViewerSettings, DataViewerSettingsService } from './DataViewerSettingsService'; -import { dataViewerManifest } from './manifest'; - -const endpoint = createGQLEndpoint(); -const app = createApp( - dataViewerManifest, - coreLocalizationManifest, - coreEventsManifest, - corePluginManifest, - coreProductManifest, - coreRootManifest, - coreSDKManifest, - coreBrowserManifest, - coreSettingsManifest, - coreViewManifest, - coreAuthenticationManifest, - coreProjectsManifest, - coreUIManifest, - coreRoutingManifest, - coreAdministrationManifest, - coreConnectionsManifest, - coreDialogsManifest, - coreNavigationTree, - coreAppManifest, - coreThemingManifest, - datasourceContextSwitchPluginManifest, - navigationTreePlugin, - navigationTabsPlugin, - objectViewerManifest, -); - -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); - -beforeAll(() => app.init()); - -const testValueA = true; -const testValueB = false; - -const equalConfigA = { - 'core.app.dataViewer': { - disableEdit: testValueA, - } as DataViewerSettings, - plugin: { - 'data-viewer': { - disableEdit: testValueA, - } as DataViewerSettings, - }, -}; - -const equalConfigB = { - 'core.app.dataViewer': { - disableEdit: testValueB, - } as DataViewerSettings, - plugin: { - 'data-viewer': { - disableEdit: testValueB, - } as DataViewerSettings, - }, -}; - -async function setupSettingsService(mockConfig: any = {}) { - const settings = app.injector.getServiceByClass(DataViewerSettingsService); - const config = app.injector.getServiceByClass(ServerConfigResource); - - server.use(endpoint.query('serverConfig', mockServerConfig(mockConfig))); - - await config.refresh(); - - return settings; -} - -test('New settings equal deprecated settings A', async () => { - const settingsService = await setupSettingsService(equalConfigA); - - expect(settingsService.settings.getValue('disableEdit')).toBe(testValueA); - expect(settingsService.deprecatedSettings.getValue('disableEdit')).toBe(testValueA); -}); - -test('New settings equal deprecated settings B', async () => { - const settingsService = await setupSettingsService(equalConfigB); - - expect(settingsService.settings.getValue('disableEdit')).toBe(testValueB); - expect(settingsService.deprecatedSettings.getValue('disableEdit')).toBe(testValueB); -}); - -describe('DataViewerSettingsService.getDefaultRowsCount', () => { - let settingsService: DataViewerSettingsService = null as any; - - beforeAll(async () => { - settingsService = await setupSettingsService({ - plugin: { - 'data-viewer': { - fetchMin: 200, - fetchMax: 1000, - fetchDefault: 300, - }, - }, - }); - }); - - test('should return valid value', () => { - expect(settingsService.getDefaultRowsCount(400)).toBe(400); - }); - - test('should return valid default value', () => { - expect(settingsService.getDefaultRowsCount()).toBe(300); - }); - - test('should return valid minimal value', () => { - expect(settingsService.getDefaultRowsCount(10)).toBe(200); - }); - - test('should return valid maximal value', () => { - expect(settingsService.getDefaultRowsCount(1100)).toBe(1000); - }); -}); +import { describe } from 'vitest'; + +// import { coreAdministrationManifest } from '@cloudbeaver/core-administration'; +// import { coreAppManifest } from '@cloudbeaver/core-app'; +// import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; +// import { mockAuthentication } from '@cloudbeaver/core-authentication/__custom_mocks__/mockAuthentication.js'; +// import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +// import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; +// import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; +// import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; +// import { coreEventsManifest } from '@cloudbeaver/core-events'; +// import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; +// import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; +// import { coreProjectsManifest } from '@cloudbeaver/core-projects'; +// import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; +// import { createGQLEndpoint } from '@cloudbeaver/core-root/__custom_mocks__/createGQLEndpoint.js'; +// import '@cloudbeaver/core-root/__custom_mocks__/expectWebsocketClosedMessage.js'; +// import { mockAppInit } from '@cloudbeaver/core-root/__custom_mocks__/mockAppInit.js'; +// import { mockGraphQL } from '@cloudbeaver/core-root/__custom_mocks__/mockGraphQL.js'; +// import { mockServerConfig } from '@cloudbeaver/core-root/__custom_mocks__/resolvers/mockServerConfig.js'; +// import { coreRoutingManifest } from '@cloudbeaver/core-routing'; +// import { coreSDKManifest } from '@cloudbeaver/core-sdk'; +// import { coreSettingsManifest } from '@cloudbeaver/core-settings'; +// import { +// expectDeprecatedSettingMessage, +// expectNoDeprecatedSettingMessage, +// } from '@cloudbeaver/core-settings/__custom_mocks__/expectDeprecatedSettingMessage.js'; +// import { coreStorageManifest } from '@cloudbeaver/core-storage'; +// import { coreUIManifest } from '@cloudbeaver/core-ui'; +// import { coreViewManifest } from '@cloudbeaver/core-view'; +// import { datasourceContextSwitchPluginManifest } from '@cloudbeaver/plugin-datasource-context-switch'; +// import { navigationTabsPlugin } from '@cloudbeaver/plugin-navigation-tabs'; +// import { navigationTreePlugin } from '@cloudbeaver/plugin-navigation-tree'; +// import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; +// import { createApp } from '@cloudbeaver/tests-runner'; + +// import { DataViewerSettingsService } from './DataViewerSettingsService.js'; +// import { dataViewerManifest } from './manifest.js'; + +// const endpoint = createGQLEndpoint(); +// const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); +// const app = createApp( +// dataViewerManifest, +// coreLocalizationManifest, +// coreEventsManifest, +// coreSDKManifest, +// coreClientActivityManifest, +// coreRootManifest, +// coreBrowserManifest, +// coreStorageManifest, +// coreSettingsManifest, +// coreViewManifest, +// coreAuthenticationManifest, +// coreProjectsManifest, +// coreUIManifest, +// coreRoutingManifest, +// coreAdministrationManifest, +// coreConnectionsManifest, +// coreDialogsManifest, +// coreNavigationTree, +// coreAppManifest, +// datasourceContextSwitchPluginManifest, +// navigationTreePlugin, +// navigationTabsPlugin, +// objectViewerManifest, +// ); + +// const testValueDeprecated = true; +// const testValueNew = false; + +// const deprecatedSettings = { +// 'core.app.dataViewer.disableEdit': testValueDeprecated, +// 'plugin.data-viewer.disabled': testValueDeprecated, +// 'plugin_data_export.disabled': testValueDeprecated, +// }; + +// const newSettings = { +// ...deprecatedSettings, +// 'plugin.data-viewer.disableEdit': testValueNew, +// 'plugin.data-viewer.export.disabled': testValueNew, +// }; + +// async function setupSettingsService(mockConfig: any = {}) { +// const settings = app.serviceProvider.getService(DataViewerSettingsService); +// const config = app.serviceProvider.getService(ServerConfigResource); + +// server.use(endpoint.query('serverConfig', mockServerConfig(mockConfig))); + +// await config.refresh(); + +// return settings; +// } + +// test('New settings override deprecated settings', async () => { +// const settingsService = await setupSettingsService(newSettings); + +// expect(settingsService.disableEdit).toBe(testValueNew); +// expect(settingsService.disableExportData).toBe(testValueNew); + +// expectNoDeprecatedSettingMessage(); +// }); + +// test('Deprecated settings are used if new settings are not defined', async () => { +// const settingsService = await setupSettingsService(deprecatedSettings); + +// expect(settingsService.disableEdit).toBe(testValueDeprecated); +// expect(settingsService.disableExportData).toBe(testValueDeprecated); + +// expectDeprecatedSettingMessage(); +// }); + +// describe('DataViewerSettingsService.getDefaultRowsCount', () => { +// let settingsService: DataViewerSettingsService = null as any; + +// beforeAll(async () => { +// settingsService = await setupSettingsService({ +// 'plugin.data-viewer.fetchMax': '1000', +// 'plugin.data-viewer.fetchDefault': '300', +// }); +// }); + +// test('should return valid value', () => { +// expect(settingsService.getDefaultRowsCount(400)).toBe(400); +// }); + +// test('should return valid default value', () => { +// expect(settingsService.getDefaultRowsCount()).toBe(300); +// }); + +// test('should return valid minimal value', () => { +// expect(settingsService.getDefaultRowsCount(10)).toBe(10); +// }); + +// test('should return valid maximal value', () => { +// expect(settingsService.getDefaultRowsCount(1100)).toBe(1000); +// }); +// }); + +describe.skip('DataViewerSettingsService', () => {}); diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts index fc8601b1e6..c841ac39f5 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts @@ -1,64 +1,172 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; -import { SettingsManagerService } from '@cloudbeaver/core-settings'; +import { HIGHEST_SETTINGS_LAYER, ServerSettingsManagerService, SettingsTransformationService } from '@cloudbeaver/core-root'; +import { + createSettingsAliasResolver, + createSettingsOverrideResolver, + ESettingsValueType, + type ISettingDescription, + ROOT_SETTINGS_LAYER, + SettingsManagerService, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; +import { DATA_EDITOR_SETTINGS_GROUP } from './DATA_EDITOR_SETTINGS_GROUP.js'; -import { DATA_EDITOR_SETTINGS_GROUP, settings } from './DATA_EDITOR_SETTINGS_GROUP'; +const FETCH_MIN = 10; +const FETCH_MAX = 5000; +const DEFAULT_FETCH_SIZE = 200; -const defaultSettings = { - disableEdit: false, - fetchMin: 100, - fetchMax: 5000, - fetchDefault: 200, -}; +const defaultSettings = schema.object({ + 'plugin.data-viewer.disableEdit': schemaExtra.stringedBoolean().default(false), + 'plugin.data-viewer.disableCopyData': schemaExtra.stringedBoolean().default(false), + 'plugin.data-viewer.fetchMax': schema.coerce.number().min(FETCH_MIN).default(FETCH_MAX), + 'resultset.maxrows': schema.coerce.number().min(FETCH_MIN).max(FETCH_MAX).default(DEFAULT_FETCH_SIZE), + 'plugin.data-viewer.export.disabled': schemaExtra.stringedBoolean().default(false), +}); -export type DataViewerSettings = typeof defaultSettings; +export type DataViewerSettingsSchema = typeof defaultSettings; +export type DataViewerSettings = schema.infer; -@injectable() +@injectable(() => [ + SettingsProviderService, + SettingsManagerService, + SettingsResolverService, + SettingsTransformationService, + ServerSettingsManagerService, +]) export class DataViewerSettingsService { - readonly settings: PluginSettings; - /** @deprecated Use settings instead, will be removed in 23.0.0 */ - readonly deprecatedSettings: PluginSettings; + get disableEdit(): boolean { + return this.settings.getValue('plugin.data-viewer.disableEdit'); + } - constructor(private readonly pluginManagerService: PluginManagerService, private readonly settingsManagerService: SettingsManagerService) { - this.settings = this.pluginManagerService.createSettings('data-viewer', 'plugin', defaultSettings); - this.deprecatedSettings = this.pluginManagerService.getDeprecatedPluginSettings('core.app.dataViewer', defaultSettings); + get disableCopyData(): boolean { + return this.settings.getValue('plugin.data-viewer.disableCopyData'); + } - settingsManagerService.addGroup(DATA_EDITOR_SETTINGS_GROUP); - settingsManagerService.addSettings(settings.scopeType, settings.scope, settings.settingsData); + get disableExportData(): boolean { + return this.settings.getValue('plugin.data-viewer.export.disabled'); } - getMaxFetchSize(): number { - if (this.settings.isValueDefault('fetchMax')) { - return this.deprecatedSettings.getValue('fetchMax'); - } - return this.settings.getValue('fetchMax'); + get maxFetchSize(): number { + return this.settings.getValue('plugin.data-viewer.fetchMax'); } - getMinFetchSize(): number { - if (this.settings.isValueDefault('fetchMin')) { - return this.deprecatedSettings.getValue('fetchMin'); - } - return this.settings.getValue('fetchMin'); + get minFetchSize(): number { + return FETCH_MIN; } - getDefaultFetchSize(): number { - if (this.settings.isValueDefault('fetchDefault')) { - return this.deprecatedSettings.getValue('fetchDefault'); - } - return this.settings.getValue('fetchDefault'); + get defaultFetchSize(): number { + return this.settings.getValue('resultset.maxrows'); + } + + readonly settings: SettingsProvider; + + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + private readonly settingsResolverService: SettingsResolverService, + private readonly settingsTransformationService: SettingsTransformationService, + private readonly serverSettingsManagerService: ServerSettingsManagerService, + ) { + // Some settings registered in plugin-data-editor-public-settings & permissions + this.settings = this.settingsProviderService.createSettings(defaultSettings); + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, + /** @deprecated Use settings instead, will be removed in 23.0.0 */ + createSettingsAliasResolver(this.settingsProviderService.settingsResolver, { + 'plugin.data-viewer.disableEdit': 'core.app.dataViewer.disableEdit', + 'plugin.data-viewer.disableCopyData': 'core.app.dataViewer.disableCopyData', + 'plugin.data-viewer.fetchMax': 'core.app.dataViewer.fetchMax', + 'plugin.data-viewer.export.disabled': 'plugin.data-export.disabled', + 'resultset.maxrows': 'core.app.dataViewer.fetchDefault', + }), + /** @deprecated Use settings instead, will be removed in 25.0.0 */ + createSettingsAliasResolver(this.settingsProviderService.settingsResolver, { + 'resultset.maxrows': 'plugin.data-viewer.fetchDefault', + }), + /** @deprecated Use settings instead, will be removed in 23.0.0 */ + createSettingsAliasResolver(this.settingsProviderService.settingsResolver, { + 'plugin.data-viewer.export.disabled': 'plugin_data_export.disabled', + }), + ); + + this.settingsResolverService.addResolver( + HIGHEST_SETTINGS_LAYER, + createSettingsOverrideResolver(this.settingsProviderService.settingsResolver, { + 'plugin.data-viewer.disableEdit': { + key: 'permission.data-editor.editing', + map: value => !value, + }, + 'plugin.data-viewer.disableCopyData': { + key: 'permission.data-editor.copy', + map: value => !value, + }, + 'plugin.data-viewer.export.disabled': { + key: 'permission.data-editor.export', + map: value => !value, + }, + }), + ); + + this.registerSettings(); } getDefaultRowsCount(count?: number): number { if (typeof count === 'number' && Number.isNaN(count)) { count = 0; } - return count !== undefined ? Math.max(this.getMinFetchSize(), Math.min(count, this.getMaxFetchSize())) : this.getDefaultFetchSize(); + return count !== undefined ? Math.max(this.minFetchSize, Math.min(count, this.maxFetchSize)) : this.defaultFetchSize; + } + + private registerSettings() { + this.settingsTransformationService.setSettingTransformer( + 'resultset.maxrows', + setting => + ({ + ...setting, + name: 'settings_data_editor_fetch_default_name', + description: 'settings_data_editor_fetch_default_description', + group: DATA_EDITOR_SETTINGS_GROUP, + }) as ISettingDescription, + ); + + this.settingsManagerService.registerSettings(() => { + const settings: ISettingDescription[] = [ + { + key: 'plugin.data-viewer.fetchMax', + access: { + scope: ['server', 'role'], + }, + type: ESettingsValueType.Input, + name: 'settings_data_editor_fetch_max_name', + description: 'settings_data_editor_fetch_max_description', + group: DATA_EDITOR_SETTINGS_GROUP, + }, + ]; + + if (!this.serverSettingsManagerService.providedSettings.has('resultset.maxrows')) { + settings.push({ + key: 'resultset.maxrows', + access: { + scope: ['server', 'client'], + }, + type: ESettingsValueType.Input, + name: 'settings_data_editor_fetch_default_name', + description: 'settings_data_editor_fetch_default_description', + group: DATA_EDITOR_SETTINGS_GROUP, + }); + } + + return settings; + }); } } diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts index 72c6b36ec1..7ae1f4304d 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts @@ -1,37 +1,54 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { ConnectionInfoResource, ConnectionsManagerService, IConnectionExecutorData } from '@cloudbeaver/core-connections'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, ConnectionsManagerService, type IConnectionExecutorData } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { INodeNavigationData, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; +import { ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { type INodeNavigationData, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; import { resourceKeyList } from '@cloudbeaver/core-resource'; -import { ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; -import { DBObjectPageService, IObjectViewerTabState, isObjectViewerTab, ObjectPage, ObjectViewerTabService } from '@cloudbeaver/plugin-object-viewer'; - -import { DataViewerPanel } from './DataViewerPage/DataViewerPanel'; -import { DataViewerTab } from './DataViewerPage/DataViewerTab'; -import { DataViewerTableService } from './DataViewerTableService'; -import type { IDataViewerPageState } from './IDataViewerPageState'; - -@injectable() +import { type ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; +import { + DBObjectPageService, + type IObjectViewerTabState, + isObjectViewerTab, + ObjectPage, + ObjectViewerTabService, +} from '@cloudbeaver/plugin-object-viewer'; + +import type { IDataViewerPageState } from './IDataViewerPageState.js'; +import { TableViewerStorageService } from './TableViewer/TableViewerStorageService.js'; + +const DataViewerTab = importLazyComponent(() => import('./DataViewerPage/DataViewerTab.js').then(module => module.DataViewerTab)); +const DataViewerPanel = importLazyComponent(() => import('./DataViewerPage/DataViewerPanel.js').then(module => module.DataViewerPanel)); + +@injectable(() => [ + NavNodeManagerService, + ObjectViewerTabService, + DBObjectPageService, + NotificationService, + ConnectionsManagerService, + NavigationTabsService, + ConnectionInfoResource, + TableViewerStorageService, +]) export class DataViewerTabService { readonly page: ObjectPage; constructor( private readonly navNodeManagerService: NavNodeManagerService, - private readonly dataViewerTableService: DataViewerTableService, private readonly objectViewerTabService: ObjectViewerTabService, private readonly dbObjectPageService: DBObjectPageService, private readonly notificationService: NotificationService, private readonly connectionsManagerService: ConnectionsManagerService, private readonly navigationTabsService: NavigationTabsService, private readonly connectionInfoResource: ConnectionInfoResource, + private readonly tableViewerStorageService: TableViewerStorageService, ) { this.page = this.dbObjectPageService.register({ key: 'data_viewer_data', @@ -40,6 +57,7 @@ export class DataViewerTabService { getTabComponent: () => DataViewerTab, getPanelComponent: () => DataViewerPanel, onRestore: this.handleTabRestore.bind(this), + onUnload: this.handleTabClose.bind(this), canClose: this.handleTabCanClose.bind(this), onClose: this.handleTabClose.bind(this), }); @@ -55,25 +73,27 @@ export class DataViewerTabService { private async disconnectHandler(data: IConnectionExecutorData, contexts: IExecutionContextProvider) { const connectionsKey = resourceKeyList(data.connections); - if (data.state === 'before') { - const tabs = Array.from( - this.navigationTabsService.findTabs( - isObjectViewerTab(tab => { - if (!tab.handlerState.connectionKey) { - return false; - } - return this.connectionInfoResource.isIntersect(connectionsKey, tab.handlerState.connectionKey); - }), - ), - ); - - for (const tab of tabs) { + const tabs = Array.from( + this.navigationTabsService.findTabs( + isObjectViewerTab(tab => { + if (!tab.handlerState.connectionKey) { + return false; + } + return this.connectionInfoResource.isIntersect(connectionsKey, tab.handlerState.connectionKey); + }), + ), + ); + + for (const tab of tabs) { + if (data.state === 'before') { const canDisconnect = await this.handleTabCanClose(tab); if (!canDisconnect) { ExecutorInterrupter.interrupt(contexts); return; } + } else if (isObjectViewerTab(tab) && tab.handlerState.tableId) { + await this.disposeTableModel(tab.handlerState.tableId); } } } @@ -94,34 +114,38 @@ export class DataViewerTabService { trySwitchPage(this.page); } } catch (exception: any) { - this.notificationService.logException(exception, 'Data Viewer Error', 'Error in Data Viewer while processing action with database node'); + this.notificationService.logException(exception, 'Data Editor Error', 'Error in Data Editor while processing action with database node'); } } - private async handleTabRestore(tab: ITab) { + private handleTabRestore(tab: ITab) { return true; } private async handleTabCanClose(tab: ITab): Promise { - const model = this.dataViewerTableService.get(tab.handlerState.tableId || ''); + const model = this.tableViewerStorageService.get(tab.handlerState.tableId || ''); if (model) { - let canClose = false; - try { - await model.requestDataAction(() => { - canClose = true; - }); - } catch {} - - return canClose; + return await model.source.canSafelyDispose(); } return true; } - private handleTabClose(tab: ITab) { + private async handleTabClose(tab: ITab) { if (tab.handlerState.tableId) { - this.dataViewerTableService.removeTableModel(tab.handlerState.tableId); + await this.disposeTableModel(tab.handlerState.tableId); + } + } + + private async disposeTableModel(tableId: string) { + if (tableId) { + const model = this.tableViewerStorageService.get(tableId); + + if (model) { + await model.dispose(); + this.tableViewerStorageService.remove(tableId); + } } } } diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts index 2ecd821dc2..e258121130 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts @@ -1,28 +1,37 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { Connection, ConnectionExecutionContextService, createConnectionParam } from '@cloudbeaver/core-connections'; -import { App, injectable } from '@cloudbeaver/core-di'; -import { EObjectFeature, NavNode, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; -import { AsyncTaskInfoService, GraphQLService } from '@cloudbeaver/core-sdk'; +import { type Connection, ConnectionExecutionContextService, createConnectionParam } from '@cloudbeaver/core-connections'; +import { injectable, IServiceProvider } from '@cloudbeaver/core-di'; +import { EObjectFeature, type NavNode, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; +import { AsyncTaskInfoService } from '@cloudbeaver/core-root'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; -import { ContainerDataSource, IDataContainerOptions } from './ContainerDataSource'; -import { DatabaseDataModel } from './DatabaseDataModel/DatabaseDataModel'; -import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel'; -import { DatabaseDataAccessMode } from './DatabaseDataModel/IDatabaseDataSource'; -import type { IDatabaseResultSet } from './DatabaseDataModel/IDatabaseResultSet'; -import { DataViewerService } from './DataViewerService'; -import { DataViewerSettingsService } from './DataViewerSettingsService'; -import { TableViewerStorageService } from './TableViewer/TableViewerStorageService'; +import { ContainerDataSource } from './ContainerDataSource.js'; +import { DatabaseDataModel } from './DatabaseDataModel/DatabaseDataModel.js'; +import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel.js'; +import { DatabaseDataAccessMode } from './DatabaseDataModel/IDatabaseDataSource.js'; +import { DataViewerService } from './DataViewerService.js'; +import { DataViewerSettingsService } from './DataViewerSettingsService.js'; +import { TableViewerStorageService } from './TableViewer/TableViewerStorageService.js'; -@injectable() +@injectable(() => [ + IServiceProvider, + NavNodeManagerService, + TableViewerStorageService, + GraphQLService, + AsyncTaskInfoService, + ConnectionExecutionContextService, + DataViewerService, + DataViewerSettingsService, +]) export class DataViewerTableService { constructor( - private readonly app: App, + private readonly serviceProvider: IServiceProvider, private readonly navNodeManagerService: NavNodeManagerService, private readonly tableViewerStorageService: TableViewerStorageService, private readonly graphQLService: GraphQLService, @@ -32,28 +41,11 @@ export class DataViewerTableService { private readonly dataViewerSettingsService: DataViewerSettingsService, ) {} - has(tableId: string): boolean { - return this.tableViewerStorageService.has(tableId); - } - - get(modelId: string): IDatabaseDataModel | undefined { - return this.tableViewerStorageService.get(modelId); - } - - async removeTableModel(tableId: string): Promise { - const model = this.tableViewerStorageService.get(tableId); - - if (model) { - this.tableViewerStorageService.remove(tableId); - await model.dispose(); - } - } - - create(connection: Connection, node: NavNode | undefined): IDatabaseDataModel { + create(connection: Connection, node: NavNode | undefined): IDatabaseDataModel { const nodeInfo = this.navNodeManagerService.getNodeContainerInfo(node?.id ?? ''); const source = new ContainerDataSource( - this.app.getServiceInjector(), + this.serviceProvider, this.graphQLService, this.asyncTaskInfoService, this.connectionExecutionContextService, diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataActionDecorator.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataActionDecorator.ts index 99a265efab..92dba3d2ed 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataActionDecorator.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataActionDecorator.ts @@ -1,16 +1,16 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { IDatabaseDataActionInterface } from '../IDatabaseDataAction'; +import 'reflect-metadata'; +import type { IDatabaseDataActionInterface } from '../IDatabaseDataAction.js'; const ACTION_PARAMS = 'custom:data-viewer/action/params'; export function databaseDataAction>() { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions return (target: U): U => { if (Reflect.hasOwnMetadata(ACTION_PARAMS, target)) { throw new Error('Duplicate databaseDataAction() decorator'); @@ -23,7 +23,6 @@ export function databaseDataAction): Function[] { return Reflect.getMetadata(ACTION_PARAMS, action) || []; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts new file mode 100644 index 0000000000..4f53fc948e --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts @@ -0,0 +1,330 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { computed, makeObservable } from 'mobx'; + +import { type DataTypeLogicalOperation, ResultDataFormat, type SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; + +import { DatabaseDataAction } from '../DatabaseDataAction.js'; +import type { IDatabaseDataOptions } from '../IDatabaseDataOptions.js'; +import type { IDatabaseDataSource } from '../IDatabaseDataSource.js'; +import type { IDatabaseResultSet } from '../IDatabaseResultSet.js'; +import { EOrder, type Order } from '../Order.js'; +import { databaseDataAction } from './DatabaseDataActionDecorator.js'; +import type { IDatabaseDataConstraintAction } from './IDatabaseDataConstraintAction.js'; + +export const IS_NULL_ID = 'IS_NULL'; +export const IS_NOT_NULL_ID = 'IS_NOT_NULL'; + +@databaseDataAction() +export class DatabaseDataConstraintAction + extends DatabaseDataAction + implements IDatabaseDataConstraintAction +{ + static dataFormat = [ResultDataFormat.Resultset, ResultDataFormat.Document]; + + get supported(): boolean { + return this.source.constraintsAvailable && this.source.results.length < 2; + } + + get orderConstraints(): SqlDataFilterConstraint[] { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + return this.source.options.constraints.filter(isOrderConstraint); + } + + get filterConstraints(): SqlDataFilterConstraint[] { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + return this.source.options.constraints.filter(isFilterConstraint); + } + + constructor(source: IDatabaseDataSource) { + super(source); + makeObservable(this, { + orderConstraints: computed, + filterConstraints: computed, + }); + } + + private deleteConstraint(attributePosition: number) { + if (!this.source.options) { + return; + } + + this.source.options.constraints = this.source.options.constraints.filter(constraint => constraint.attributePosition !== attributePosition); + } + + private deleteEmptyConstraint(attributePosition: number) { + const constraint = this.get(attributePosition); + + if (constraint && !isFilterConstraint(constraint) && !isOrderConstraint(constraint)) { + this.deleteConstraint(attributePosition); + } + } + + private getMaxOrderPosition() { + return Math.max(0, ...this.orderConstraints.map(constraint => (constraint.orderPosition !== undefined ? constraint.orderPosition + 1 : -1))); + } + + get(attributePosition: number): SqlDataFilterConstraint | undefined { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + return this.source.options.constraints.find(constraint => constraint.attributePosition === attributePosition); + } + + deleteAll(): void { + if (!this.source.options) { + return; + } + + this.source.options.constraints = []; + } + + deleteFilter(attributePosition: number): void { + const constraint = this.get(attributePosition); + if (constraint) { + deleteLogicalOperationFromConstraint(constraint); + this.deleteEmptyConstraint(attributePosition); + } + } + + deleteFilters(): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const newConstraints: SqlDataFilterConstraint[] = []; + + for (const constraint of this.source.options.constraints) { + deleteLogicalOperationFromConstraint(constraint); + if (isOrderConstraint(constraint)) { + newConstraints.push(constraint); + } + } + + this.source.options.constraints = newConstraints; + } + + deleteOrders(): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const newConstraints: SqlDataFilterConstraint[] = []; + + for (const constraint of this.source.options.constraints) { + deleteOrderFromConstraint(constraint); + if (isFilterConstraint(constraint)) { + newConstraints.push(constraint); + } + } + + this.source.options.constraints = newConstraints; + } + + deleteOrder(attributePosition: number): void { + const constraint = this.get(attributePosition); + if (constraint) { + deleteOrderFromConstraint(constraint); + this.deleteEmptyConstraint(attributePosition); + } + } + + deleteDataFilters(): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + this.deleteFilters(); + this.resetWhereFilter(); + } + + deleteData(): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + this.deleteAll(); + this.resetWhereFilter(); + } + + setWhereFilter(value: string) { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + this.source.options.whereFilter = value; + } + + resetWhereFilter() { + this.setWhereFilter(''); + } + + setFilter(attributePosition: number, operator: string, value?: any): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const currentConstraint = this.get(attributePosition); + + if (currentConstraint) { + currentConstraint.operator = operator; + if (value !== undefined) { + currentConstraint.value = value; + } else if (currentConstraint.value !== undefined) { + delete currentConstraint.value; + } + return; + } + + const constraint: SqlDataFilterConstraint = { + attributePosition, + operator, + }; + + if (value !== undefined) { + constraint.value = value; + } + + this.source.options.constraints.push(constraint); + } + + setOrder(attributePosition: number, order: Order, multiple: boolean): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const resetOrder = order === null; + + if (!multiple) { + this.deleteOrders(); + } + + const currentConstraint = this.get(attributePosition); + + if (!currentConstraint) { + if (!resetOrder) { + this.source.options.constraints.push({ + attributePosition, + orderPosition: this.getMaxOrderPosition(), + orderAsc: order === EOrder.asc, + }); + } + return; + } + + if (!resetOrder) { + if (!isOrderConstraint(currentConstraint)) { + currentConstraint.orderPosition = this.getMaxOrderPosition(); + } + currentConstraint.orderAsc = order === EOrder.asc; + } else { + if (isFilterConstraint(currentConstraint)) { + deleteOrderFromConstraint(currentConstraint); + } else { + this.deleteConstraint(currentConstraint.attributePosition); + } + } + } + + getOrder(attributePosition: number): Order { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const currentConstraint = this.get(attributePosition); + + if (!currentConstraint || !isOrderConstraint(currentConstraint)) { + return null; + } + + return currentConstraint.orderAsc ? EOrder.asc : EOrder.desc; + } + + override updateResults(results: IDatabaseResultSet[]): void { + const nextResult = results[this.resultIndex]; + if (!this.source.options || results.length !== this.source.results.length || !nextResult) { + return; + } + + for (const constraint of this.source.options.constraints) { + const prevColumn = this.result.data?.columns?.find(column => column.position === constraint.attributePosition); + + if (!prevColumn) { + return; + } + + let column = nextResult.data?.columns?.find(column => column.position === prevColumn.position); + + if (!column || column.label !== prevColumn.label) { + column = nextResult.data?.columns?.find(column => column.label === prevColumn.label); + } + + if (column && prevColumn.position !== column.position) { + const prevConstraint = this.source.prevOptions?.constraints.find( + prevConstraint => prevConstraint.attributePosition === constraint.attributePosition, + ); + + constraint.attributePosition = column.position; + + if (prevConstraint) { + prevConstraint.attributePosition = constraint.attributePosition; + } + } + } + } +} + +export function nullOperationsFilter(operation: DataTypeLogicalOperation): boolean { + return operation.id === IS_NULL_ID || operation.id === IS_NOT_NULL_ID; +} + +export function getNextOrder(order: Order): Order { + switch (order) { + case EOrder.asc: + return EOrder.desc; + case EOrder.desc: + return null; + default: + return EOrder.asc; + } +} + +export function wrapOperationArgument(operationId: string, argument: any): string { + if (operationId === 'LIKE') { + return `%${argument}%`; + } + + return argument; +} + +export function isFilterConstraint(constraint: SqlDataFilterConstraint): boolean { + return constraint.operator !== undefined; +} + +export function isOrderConstraint(constraint: SqlDataFilterConstraint): boolean { + return constraint.orderAsc !== undefined; +} + +function deleteOrderFromConstraint(constraint: SqlDataFilterConstraint) { + delete constraint.orderAsc; + delete constraint.orderPosition; + return constraint; +} + +function deleteLogicalOperationFromConstraint(constraint: SqlDataFilterConstraint) { + delete constraint.operator; + delete constraint.value; + return constraint; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataResultAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataResultAction.ts new file mode 100644 index 0000000000..e63805b15f --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataResultAction.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; + +import { DatabaseDataAction } from '../DatabaseDataAction.js'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult.js'; +import type { IDatabaseDataSource } from '../IDatabaseDataSource.js'; +import { databaseDataAction } from './DatabaseDataActionDecorator.js'; +import type { IDatabaseDataResultAction } from './IDatabaseDataResultAction.js'; + +@databaseDataAction() +export abstract class DatabaseDataResultAction + extends DatabaseDataAction + implements IDatabaseDataResultAction +{ + static dataFormat: ResultDataFormat[] | null = null; + + get empty(): boolean { + return !this.result.data; + } + + constructor(source: IDatabaseDataSource) { + super(source); + } + abstract getIdentifier(key: TKey): string; + abstract serialize(key: TKey): string; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseEditAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseEditAction.ts index aa4a558fce..7c3b276530 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseEditAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseEditAction.ts @@ -1,23 +1,23 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { DatabaseDataAction } from '../DatabaseDataAction'; -import type { IDatabaseDataResult } from '../IDatabaseDataResult'; -import type { IDatabaseDataSource } from '../IDatabaseDataSource'; -import { databaseDataAction } from './DatabaseDataActionDecorator'; +import { DatabaseDataAction } from '../DatabaseDataAction.js'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult.js'; +import type { IDatabaseDataSource } from '../IDatabaseDataSource.js'; +import { databaseDataAction } from './DatabaseDataActionDecorator.js'; import type { DatabaseEditChangeType, IDatabaseDataEditAction, IDatabaseDataEditActionData, IDatabaseDataEditApplyActionData, -} from './IDatabaseDataEditAction'; +} from './IDatabaseDataEditAction.js'; @databaseDataAction() export abstract class DatabaseEditAction @@ -49,6 +49,7 @@ export abstract class DatabaseEditAction extends DatabaseDataAction implements IDatabaseDataMetadataAction { + static dataFormat: ResultDataFormat[] | null = null; + readonly metadata: MetadataMap; + + constructor(source: IDatabaseDataSource) { + super(source); + this.metadata = new MetadataMap(); + } + + has(key: string): boolean { + return this.metadata.has(key); + } + + get(key: string): T | undefined; + get(key: string, getDefaultValue: (() => T) | undefined): T; + get(key: string, getDefaultValue?: (() => T) | undefined): T | undefined { + return this.metadata.get(key, getDefaultValue); + } + + set(key: string, value: any): void { + this.metadata.set(key, value); + } + + delete(key: string): void { + this.metadata.delete(key); + } +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseRefreshAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseRefreshAction.ts new file mode 100644 index 0000000000..82640825b8 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseRefreshAction.ts @@ -0,0 +1,111 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observable } from 'mobx'; + +import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; + +import { DatabaseDataAction } from '../DatabaseDataAction.js'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult.js'; +import type { IDatabaseDataSource } from '../IDatabaseDataSource.js'; +import { databaseDataAction } from './DatabaseDataActionDecorator.js'; + +export interface IDatabaseRefreshState { + interval: number; + paused: boolean; + stopOnError: boolean; +} + +@databaseDataAction() +export class DatabaseRefreshAction extends DatabaseDataAction { + static dataFormat: ResultDataFormat[] | null = null; + + get isAutoRefresh(): boolean { + return this.state.interval > 0; + } + + get interval(): number { + return this.state.interval; + } + + get paused(): boolean { + return this.state.paused; + } + + get stopOnError(): boolean { + return this.state.stopOnError; + } + + private state: IDatabaseRefreshState; + private timer: ReturnType | null; + constructor(source: IDatabaseDataSource) { + super(source); + this.state = observable({ interval: 0, paused: false, stopOnError: true }); + this.timer = null; + } + + setInterval(interval: number): void { + this.state.interval = interval; + + if (this.state.interval) { + this.startTimer(); + this.resume(); + } else { + this.stopTimer(); + } + } + + setStopOnError(stopOnError: boolean): void { + this.state.stopOnError = stopOnError; + } + + pause(): void { + this.state.paused = true; + } + + resume(): void { + this.state.paused = false; + } + + override dispose(): void { + this.stopTimer(); + } + + private stopTimer(): void { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + private startTimer(): void { + if (this.state.interval <= 0) { + return; + } + if (this.timer) { + this.stopTimer(); + } + this.timer = setTimeout(this.refresh.bind(this), this.state.interval); + } + + private async refresh(): Promise { + if (this.state.paused) { + this.startTimer(); + return; + } + try { + await this.source.refreshData(); + this.startTimer(); + } catch (exception) { + if (this.state.stopOnError) { + this.setInterval(0); + } else { + this.startTimer(); + } + } + } +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseSelectAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseSelectAction.ts index b2b2e2053a..c49240d2c4 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseSelectAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseSelectAction.ts @@ -1,21 +1,21 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { DatabaseDataAction } from '../DatabaseDataAction'; -import type { IDatabaseDataResult } from '../IDatabaseDataResult'; -import type { IDatabaseDataSource } from '../IDatabaseDataSource'; -import { databaseDataAction } from './DatabaseDataActionDecorator'; -import type { DatabaseDataSelectActionsData, IDatabaseDataSelectAction } from './IDatabaseDataSelectAction'; +import { DatabaseDataAction } from '../DatabaseDataAction.js'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult.js'; +import type { IDatabaseDataSource } from '../IDatabaseDataSource.js'; +import { databaseDataAction } from './DatabaseDataActionDecorator.js'; +import type { DatabaseDataSelectActionsData, IDatabaseDataSelectAction } from './IDatabaseDataSelectAction.js'; @databaseDataAction() -export abstract class DatabaseSelectAction +export abstract class DatabaseSelectAction extends DatabaseDataAction implements IDatabaseDataSelectAction { @@ -29,9 +29,11 @@ export abstract class DatabaseSelectAction implements IDatabaseDataResultAction { - static dataFormat = [ResultDataFormat.Document]; +export class DocumentDataAction extends DatabaseDataResultAction { + static override dataFormat = [ResultDataFormat.Document]; get documents(): IDatabaseDataDocument[] { - return this.result.data?.rows?.map(row => row[0]) || []; + return this.result.data?.rowsWithMetaData?.map(row => row.data[0]) || []; } get count(): number { - return this.result.data?.rows?.length || 0; + return this.result.data?.rowsWithMetaData?.length || 0; } constructor(source: IDatabaseDataSource) { @@ -37,6 +37,19 @@ export class DocumentDataAction extends DatabaseDataAction row.data[0]?.id === documentId); + return row?.metaData; + } + + getIdentifier(key: IDocumentElementKey): string { + return key.index.toString(); + } + + serialize(key: IDocumentElementKey): string { + return key.index.toString(); + } + get(index: number): IDatabaseDataDocument | undefined { if (this.documents.length <= index) { return undefined; @@ -46,8 +59,12 @@ export class DocumentDataAction extends DatabaseDataAction { - static dataFormat = [ResultDataFormat.Document]; + static override dataFormat = [ResultDataFormat.Document]; readonly editedElements: Map; private readonly data: DocumentDataAction; @@ -121,11 +121,26 @@ export class DocumentEditAction extends DatabaseEditAction extends IDatabaseDataAction { + has(key: TKey, scope: symbol): boolean; + get(key: TKey, scope: symbol): T | undefined; + set(key: TKey, scope: symbol, value: T): void; + delete(key: TKey, scope: symbol): void; + deleteAll(scope: symbol): void; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts index e403758390..8b3b5c6123 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts @@ -1,15 +1,15 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; -import type { IDatabaseDataAction } from '../IDatabaseDataAction'; -import type { IDatabaseDataResult } from '../IDatabaseDataResult'; -import type { Order } from '../Order'; +import type { IDatabaseDataAction } from '../IDatabaseDataAction.js'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult.js'; +import type { Order } from '../Order.js'; export interface IDatabaseDataConstraintAction extends IDatabaseDataAction { readonly filterConstraints: SqlDataFilterConstraint[]; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataEditAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataEditAction.ts index 687b7c6da2..97a8fe8206 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataEditAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataEditAction.ts @@ -1,20 +1,20 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { ISyncExecutor } from '@cloudbeaver/core-executor'; -import type { IDatabaseDataAction } from '../IDatabaseDataAction'; -import type { IDatabaseDataResult } from '../IDatabaseDataResult'; +import type { IDatabaseDataAction } from '../IDatabaseDataAction.js'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult.js'; // order is matter, used for sorting and changes diff export enum DatabaseEditChangeType { - update, - add, - delete, + update = 0, + add = 1, + delete = 2, } export interface IDatabaseDataEditActionValue { @@ -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..e275bd74d8 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts @@ -1,17 +1,18 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IDatabaseDataAction } from '../IDatabaseDataAction'; -import type { IDatabaseDataResult } from '../IDatabaseDataResult'; +import type { IDatabaseDataAction } from '../IDatabaseDataAction.js'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult.js'; 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/IDatabaseDataMetadataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataMetadataAction.ts new file mode 100644 index 0000000000..8053e0fa6d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataMetadataAction.ts @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IDatabaseDataAction } from '../IDatabaseDataAction.js'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult.js'; + +export interface IDatabaseDataMetadataAction extends IDatabaseDataAction { + get(key: string): T | undefined; + get(key: string, getDefaultValue: () => T): T; + set(key: string, value: any): void; + delete(key: string): void; + has(key: string): boolean; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataResultAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataResultAction.ts index c74490c590..d970cb3628 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataResultAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataResultAction.ts @@ -1,11 +1,15 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IDatabaseDataAction } from '../IDatabaseDataAction'; -import type { IDatabaseDataResult } from '../IDatabaseDataResult'; +import type { IDatabaseDataAction } from '../IDatabaseDataAction.js'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult.js'; -export type IDatabaseDataResultAction = IDatabaseDataAction; +export interface IDatabaseDataResultAction extends IDatabaseDataAction { + readonly empty: boolean; + getIdentifier(key: TKey): string; + serialize(key: TKey): string; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSelectAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSelectAction.ts index 4bfe142982..a49645ff64 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSelectAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSelectAction.ts @@ -1,14 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { ISyncExecutor } from '@cloudbeaver/core-executor'; -import type { IDatabaseDataAction } from '../IDatabaseDataAction'; -import type { IDatabaseDataResult } from '../IDatabaseDataResult'; +import type { IDatabaseDataAction } from '../IDatabaseDataAction.js'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult.js'; export type DatabaseDataSelectActionsData = | { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_CONSTRAINTS_DELETE_ACTION.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_CONSTRAINTS_DELETE_ACTION.ts index 1270f3eef7..b32039372e 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_CONSTRAINTS_DELETE_ACTION.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_CONSTRAINTS_DELETE_ACTION.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_REFRESH_ACTION.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_REFRESH_ACTION.ts index 93b469677a..d00d668e5b 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_REFRESH_ACTION.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_REFRESH_ACTION.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/DATA_CONTEXT_DV_RESULT_KEY.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/DATA_CONTEXT_DV_RESULT_KEY.ts new file mode 100644 index 0000000000..a94c193f9a --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/DATA_CONTEXT_DV_RESULT_KEY.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createDataContext } from '@cloudbeaver/core-data-context'; + +import type { IResultSetElementKey } from './IResultSetDataKey.js'; + +export const DATA_CONTEXT_DV_RESULT_KEY = createDataContext('data-viewer-database-result-key'); diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/DataContext/DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/DataContext/DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY.ts index 95991dcffe..9bf0b811f1 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/DataContext/DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/DataContext/DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY.ts @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { createDataContext } from '@cloudbeaver/core-data-context'; -import type { IResultSetColumnKey } from '../IResultSetDataKey'; +import type { IResultSetColumnKey } from '../IResultSetDataKey.js'; export const DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY = createDataContext( 'data-viewer-database-data-model-result-set-column-key', 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..3a0c169630 --- /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-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IResultSetFileValue } from './IResultSetFileValue.js'; + +export interface IResultSetBlobValue extends IResultSetFileValue { + blob: Blob; +} 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 deleted file mode 100644 index 8f32f4d009..0000000000 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetContentValue.ts +++ /dev/null @@ -1,15 +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. - */ - -export interface IResultSetContentValue { - $type: 'content'; - binary?: string; - text?: string; - contentType?: string; - contentLength?: number; -} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataContentAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataContentAction.ts index c4d547220c..5f648ac3b5 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataContentAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataContentAction.ts @@ -1,20 +1,19 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IResultSetContentValue } from './IResultSetContentValue'; -import type { IResultSetElementKey } from './IResultSetDataKey'; +import type { IResultSetElementKey } from './IResultSetDataKey.js'; export interface IResultSetDataContentAction { - activeElement: IResultSetElementKey | null; - isContentTruncated: (content: IResultSetContentValue) => boolean; + isLoading: (element: IResultSetElementKey) => boolean; + isBlobTruncated: (element: IResultSetElementKey) => boolean; + isTextTruncated: (element: IResultSetElementKey) => boolean; isDownloadable: (element: IResultSetElementKey) => boolean; - getFileDataUrl: (element: IResultSetElementKey) => Promise; - resolveFileDataUrl: (element: IResultSetElementKey) => Promise; - retrieveFileDataUrlFromCache: (element: IResultSetElementKey) => string | undefined; + resolveFileDataUrl: (element: IResultSetElementKey) => Promise; + retrieveBlobFromCache: (element: IResultSetElementKey) => Blob | undefined; downloadFileData: (element: IResultSetElementKey) => Promise; clearCache: () => void; } 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..1263c5ab3e 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 @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -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..92d3f67739 --- /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-2025 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 '@dbeaver/result-set-api'; + +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..c91f503323 --- /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-2025 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 '@dbeaver/result-set-api'; + +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/ResultSetCacheAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts new file mode 100644 index 0000000000..5a04f04691 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts @@ -0,0 +1,164 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { action, makeObservable, observable } from 'mobx'; + +import { ResultDataFormat } from '@cloudbeaver/core-sdk'; + +import { DatabaseDataAction } from '../../DatabaseDataAction.js'; +import type { IDatabaseDataSource } from '../../IDatabaseDataSource.js'; +import type { IDatabaseResultSet } from '../../IDatabaseResultSet.js'; +import { databaseDataAction } from '../DatabaseDataActionDecorator.js'; +import type { IDatabaseDataCacheAction } from '../IDatabaseDataCacheAction.js'; +import type { IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey.js'; +import { ResultSetDataAction } from './ResultSetDataAction.js'; + +@databaseDataAction() +export class ResultSetCacheAction + extends DatabaseDataAction + implements IDatabaseDataCacheAction +{ + static dataFormat = [ResultDataFormat.Resultset]; + + private readonly cache: Map>; + + constructor( + source: IDatabaseDataSource, + private readonly data: ResultSetDataAction, + ) { + super(source); + + this.cache = new Map(); + + makeObservable(this, { + cache: observable, + set: action, + setRow: action, + delete: action, + deleteAll: action, + deleteRow: action, + }); + } + + get(key: IResultSetElementKey, scope: symbol): T | undefined { + const keyCache = this.getKeyCache(key); + if (!keyCache) { + return; + } + + return keyCache.get(scope); + } + + getRow(key: IResultSetRowKey, scope: symbol): T | undefined { + const keyCache = this.getRowCache(key); + if (!keyCache) { + return; + } + + return keyCache.get(scope); + } + + has(key: IResultSetElementKey, scope: symbol) { + const keyCache = this.getKeyCache(key); + + if (!keyCache) { + return false; + } + + return keyCache.has(scope); + } + + hasRow(key: IResultSetRowKey, scope: symbol) { + const keyCache = this.getRowCache(key); + + if (!keyCache) { + return false; + } + + return keyCache.has(scope); + } + + set(key: IResultSetElementKey, scope: symbol, value: T) { + const keyCache = this.getOrCreateKeyCache(key); + + keyCache.set(scope, value); + } + + setRow(key: IResultSetRowKey, scope: symbol, value: T) { + const keyCache = this.getOrCreateRowKeyCache(key); + + keyCache.set(scope, value); + } + + delete(key: IResultSetElementKey, scope: symbol) { + const keyCache = this.getKeyCache(key); + + if (keyCache) { + keyCache.delete(scope); + } + } + + deleteAll(scope: symbol) { + for (const [, keyCache] of this.cache) { + keyCache.delete(scope); + } + } + + deleteRow(key: IResultSetRowKey, scope: symbol) { + const keyCache = this.getRowCache(key); + + if (keyCache) { + keyCache.delete(scope); + } + } + + override afterResultUpdate() { + this.cache.clear(); + } + + override dispose(): void { + this.cache.clear(); + } + + private serializeRowKey(key: IResultSetRowKey) { + return 'row:' + this.data.serializeRowKey(key); + } + + private serializeKey(key: IResultSetElementKey) { + return this.data.serialize(key); + } + + private getKeyCache(key: IResultSetElementKey) { + return this.cache.get(this.serializeKey(key)); + } + + private getRowCache(key: IResultSetRowKey) { + return this.cache.get(this.serializeRowKey(key)); + } + + private getOrCreateKeyCache(key: IResultSetElementKey) { + let keyCache = this.getKeyCache(key); + + if (!keyCache) { + keyCache = observable(new Map()); + this.cache.set(this.serializeKey(key), keyCache); + } + + return keyCache; + } + + private getOrCreateRowKeyCache(key: IResultSetRowKey) { + let keyCache = this.getRowCache(key); + + if (!keyCache) { + keyCache = observable(new Map()); + this.cache.set(this.serializeRowKey(key), keyCache); + } + + return keyCache; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts deleted file mode 100644 index 4bbf811bec..0000000000 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts +++ /dev/null @@ -1,330 +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 { computed, makeObservable } from 'mobx'; - -import { DataTypeLogicalOperation, ResultDataFormat, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; - -import { DatabaseDataAction } from '../../DatabaseDataAction'; -import type { IDatabaseDataOptions } from '../../IDatabaseDataOptions'; -import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; -import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; -import { EOrder, Order } from '../../Order'; -import { databaseDataAction } from '../DatabaseDataActionDecorator'; -import type { IDatabaseDataConstraintAction } from '../IDatabaseDataConstraintAction'; - -export const IS_NULL_ID = 'IS_NULL'; -export const IS_NOT_NULL_ID = 'IS_NOT_NULL'; - -@databaseDataAction() -export class ResultSetConstraintAction - extends DatabaseDataAction - implements IDatabaseDataConstraintAction -{ - static dataFormat = [ResultDataFormat.Resultset, ResultDataFormat.Document]; - - get supported(): boolean { - return this.source.constraintsAvailable && this.source.results.length < 2; - } - - get orderConstraints(): SqlDataFilterConstraint[] { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - return this.source.options.constraints.filter(isOrderConstraint); - } - - get filterConstraints(): SqlDataFilterConstraint[] { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - return this.source.options.constraints.filter(isFilterConstraint); - } - - constructor(source: IDatabaseDataSource) { - super(source); - makeObservable(this, { - orderConstraints: computed, - filterConstraints: computed, - }); - } - - private deleteConstraint(attributePosition: number) { - if (!this.source.options) { - return; - } - - this.source.options.constraints = this.source.options.constraints.filter(constraint => constraint.attributePosition !== attributePosition); - } - - private deleteEmptyConstraint(attributePosition: number) { - const constraint = this.get(attributePosition); - - if (constraint && !isFilterConstraint(constraint) && !isOrderConstraint(constraint)) { - this.deleteConstraint(attributePosition); - } - } - - private getMaxOrderPosition() { - return Math.max(0, ...this.orderConstraints.map(constraint => (constraint.orderPosition !== undefined ? constraint.orderPosition + 1 : -1))); - } - - get(attributePosition: number): SqlDataFilterConstraint | undefined { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - return this.source.options.constraints.find(constraint => constraint.attributePosition === attributePosition); - } - - deleteAll(): void { - if (!this.source.options) { - return; - } - - this.source.options.constraints = []; - } - - deleteFilter(attributePosition: number): void { - const constraint = this.get(attributePosition); - if (constraint) { - deleteLogicalOperationFromConstraint(constraint); - this.deleteEmptyConstraint(attributePosition); - } - } - - deleteFilters(): void { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - const newConstraints: SqlDataFilterConstraint[] = []; - - for (const constraint of this.source.options.constraints) { - deleteLogicalOperationFromConstraint(constraint); - if (isOrderConstraint(constraint)) { - newConstraints.push(constraint); - } - } - - this.source.options.constraints = newConstraints; - } - - deleteOrders(): void { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - const newConstraints: SqlDataFilterConstraint[] = []; - - for (const constraint of this.source.options.constraints) { - deleteOrderFromConstraint(constraint); - if (isFilterConstraint(constraint)) { - newConstraints.push(constraint); - } - } - - this.source.options.constraints = newConstraints; - } - - deleteOrder(attributePosition: number): void { - const constraint = this.get(attributePosition); - if (constraint) { - deleteOrderFromConstraint(constraint); - this.deleteEmptyConstraint(attributePosition); - } - } - - deleteDataFilters(): void { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - this.deleteFilters(); - this.resetWhereFilter(); - } - - deleteData(): void { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - this.deleteAll(); - this.resetWhereFilter(); - } - - setWhereFilter(value: string) { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - this.source.options.whereFilter = value; - } - - resetWhereFilter() { - this.setWhereFilter(''); - } - - setFilter(attributePosition: number, operator: string, value?: any): void { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - const currentConstraint = this.get(attributePosition); - - if (currentConstraint) { - currentConstraint.operator = operator; - if (value !== undefined) { - currentConstraint.value = value; - } else if (currentConstraint.value !== undefined) { - delete currentConstraint.value; - } - return; - } - - const constraint: SqlDataFilterConstraint = { - attributePosition, - operator, - }; - - if (value !== undefined) { - constraint.value = value; - } - - this.source.options.constraints.push(constraint); - } - - setOrder(attributePosition: number, order: Order, multiple: boolean): void { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - const resetOrder = order === null; - - if (!multiple) { - this.deleteOrders(); - } - - const currentConstraint = this.get(attributePosition); - - if (!currentConstraint) { - if (!resetOrder) { - this.source.options.constraints.push({ - attributePosition, - orderPosition: this.getMaxOrderPosition(), - orderAsc: order === EOrder.asc, - }); - } - return; - } - - if (!resetOrder) { - if (!isOrderConstraint(currentConstraint)) { - currentConstraint.orderPosition = this.getMaxOrderPosition(); - } - currentConstraint.orderAsc = order === EOrder.asc; - } else { - if (isFilterConstraint(currentConstraint)) { - deleteOrderFromConstraint(currentConstraint); - } else { - this.deleteConstraint(currentConstraint.attributePosition); - } - } - } - - getOrder(attributePosition: number): Order { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - const currentConstraint = this.get(attributePosition); - - if (!currentConstraint || !isOrderConstraint(currentConstraint)) { - return null; - } - - return currentConstraint.orderAsc ? EOrder.asc : EOrder.desc; - } - - updateResults(results: IDatabaseResultSet[]): void { - const nextResult = results[this.resultIndex]; - if (!this.source.options || results.length !== this.source.results.length || !nextResult) { - return; - } - - for (const constraint of this.source.options.constraints) { - const prevColumn = this.result.data?.columns?.find(column => column.position === constraint.attributePosition); - - if (!prevColumn) { - return; - } - - let column = nextResult.data?.columns?.find(column => column.position === prevColumn.position); - - if (!column || column.label !== prevColumn.label) { - column = nextResult.data?.columns?.find(column => column.label === prevColumn.label); - } - - if (column && prevColumn.position !== column.position) { - const prevConstraint = this.source.prevOptions?.constraints.find( - prevConstraint => prevConstraint.attributePosition === constraint.attributePosition, - ); - - constraint.attributePosition = column.position; - - if (prevConstraint) { - prevConstraint.attributePosition = constraint.attributePosition; - } - } - } - } -} - -export function nullOperationsFilter(operation: DataTypeLogicalOperation): boolean { - return operation.id === IS_NULL_ID || operation.id === IS_NOT_NULL_ID; -} - -export function getNextOrder(order: Order): Order { - switch (order) { - case EOrder.asc: - return EOrder.desc; - case EOrder.desc: - return null; - default: - return EOrder.asc; - } -} - -export function wrapOperationArgument(operationId: string, argument: any): string { - if (operationId === 'LIKE') { - return `%${argument}%`; - } - - return argument; -} - -export function isFilterConstraint(constraint: SqlDataFilterConstraint): boolean { - return constraint.operator !== undefined; -} - -export function isOrderConstraint(constraint: SqlDataFilterConstraint): boolean { - return constraint.orderAsc !== undefined; -} - -function deleteOrderFromConstraint(constraint: SqlDataFilterConstraint) { - delete constraint.orderAsc; - delete constraint.orderPosition; - return constraint; -} - -function deleteLogicalOperationFromConstraint(constraint: SqlDataFilterConstraint) { - delete constraint.operator; - delete constraint.value; - return constraint; -} 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..d82afeccc3 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 @@ -1,30 +1,29 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { DataTypeLogicalOperation, ResultDataFormat, SqlResultColumn } from '@cloudbeaver/core-sdk'; +import { type DataTypeLogicalOperation, ResultDataFormat, type SqlResultColumn } from '@cloudbeaver/core-sdk'; +import { isResultSetContentValue, type IResultSetContentValue } from '@dbeaver/result-set-api'; -import { DatabaseDataAction } from '../../DatabaseDataAction'; -import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; -import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; -import { databaseDataAction } from '../DatabaseDataActionDecorator'; -import type { IDatabaseDataResultAction } from '../IDatabaseDataResultAction'; -import type { IResultSetContentValue } from './IResultSetContentValue'; -import type { IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey'; -import { isResultSetContentValue } from './isResultSetContentValue'; -import type { IResultSetValue } from './ResultSetFormatAction'; +import type { IDatabaseDataSource } from '../../IDatabaseDataSource.js'; +import type { IDatabaseResultSet } from '../../IDatabaseResultSet.js'; +import { databaseDataAction } from '../DatabaseDataActionDecorator.js'; +import { DatabaseDataResultAction } from '../DatabaseDataResultAction.js'; +import type { IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey.js'; +import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils.js'; +import type { IResultSetValue } from './ResultSetFormatAction.js'; @databaseDataAction() -export class ResultSetDataAction extends DatabaseDataAction implements IDatabaseDataResultAction { - static dataFormat = [ResultDataFormat.Resultset]; +export class ResultSetDataAction extends DatabaseDataResultAction { + static override dataFormat = [ResultDataFormat.Resultset]; - get rows(): IResultSetValue[][] { - return this.result.data?.rows || []; + get rows() { + return this.result.data?.rowsWithMetaData || []; } get columns(): SqlResultColumn[] { @@ -39,10 +38,23 @@ export class ResultSetDataAction extends DatabaseDataAction= this.rows.length) { + return undefined; + } + + return this.rows[row.index]?.metaData; } getCellValue(cell: IResultSetElementKey): IResultSetValue | undefined { @@ -90,7 +110,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; + private subscriptionDispose?: () => void; constructor( source: IDatabaseDataSource, - view: ResultSetViewAction, - data: ResultSetDataAction, - graphQLService: GraphQLService, - quotasService: QuotasService, + private readonly data: ResultSetDataAction, + private readonly format: ResultSetFormatAction, + private readonly graphQLService: GraphQLService, + private readonly serverResourceQuotasResource: ServerResourceQuotasResource, + private readonly quotasService: QuotasService, + private readonly cache: ResultSetCacheAction, ) { super(source); - this.view = view; - this.data = data; + function loadQuotas() { + setTimeout(() => serverResourceQuotasResource.load(), 0); + } + + this.serverResourceQuotasResource.onDataOutdated.addHandler(loadQuotas); - this.graphQLService = graphQLService; - this.quotasService = quotasService; + loadQuotas(); - this.cache = new Map(); - this.activeElement = null; + this.subscriptionDispose = () => { + this.serverResourceQuotasResource.onDataOutdated.removeHandler(loadQuotas); + }; makeObservable(this, { cache: observable, - activeElement: observable.ref, }); } - isContentTruncated(content: IResultSetContentValue) { - return (content.contentLength ?? 0) > this.quotasService.getQuota('sqlBinaryPreviewMaxLength'); + getLimitInfo(elementKey: IResultSetElementKey) { + const isTextColumn = this.format.isText(elementKey); + const isBlob = this.format.isBinary(elementKey); + const result = { + limit: undefined as number | undefined, + limitWithSize: undefined as string | undefined, + }; + + if (isTextColumn) { + result.limit = this.quotasService.getQuota('sqlTextPreviewMaxLength'); + } + + if (isBlob) { + result.limit = this.quotasService.getQuota('sqlBinaryPreviewMaxLength'); + } + + if (result.limit) { + result.limitWithSize = bytesToSize(result.limit); + } + + return result; + } + + isLoading(element: IResultSetElementKey) { + return this.getCache(element)?.loading ?? false; + } + + isBlobTruncated(elementKey: IResultSetElementKey) { + const limit = this.getLimitInfo(elementKey).limit; + const content = this.format.get(elementKey); + + if (!isNotNullDefined(limit) || !isResultSetContentValue(content) || !this.format.isBinary(elementKey)) { + return false; + } + + return (content.contentLength ?? 0) > limit; + } + + isTextTruncated(elementKey: IResultSetElementKey) { + const limit = this.getLimitInfo(elementKey).limit; + const content = this.format.get(elementKey); + + if (!isNotNullDefined(limit) || !isResultSetContentValue(content)) { + return false; + } + + return (content.contentLength ?? 0) > limit; } isDownloadable(element: IResultSetElementKey) { - const cellValue = this.view.getCellValue(element); - return !!this.result.data?.hasRowIdentifier && isResultSetContentValue(cellValue); + return !!this.result.data?.hasRowIdentifier && isResultSetContentValue(this.format.get(element)); } - async getFileDataUrl(element: IResultSetElementKey) { + retrieveFullTextFromCache(element: IResultSetElementKey) { + return this.getCache(element)?.fullText; + } + + retrieveBlobFromCache(element: IResultSetElementKey) { + return this.getCache(element)?.blob; + } + + async getFileFullText(element: IResultSetElementKey) { const column = this.data.getColumn(element.column); const row = this.data.getRowValue(element.row); + const cachedFullText = this.retrieveFullTextFromCache(element); + + if (cachedFullText) { + return cachedFullText; + } + if (!row || !column) { throw new Error('Failed to get value metadata information'); } - const url = await this.source.runTask(async () => { + const fullText = await this.source.runOperation(async () => { try { - this.activeElement = element; - const fileName = await this.loadFileName(this.result, column.position, row); - return this.generateFileDataUrl(fileName); + this.updateCache(element, { loading: true }); + return await this.loadFileFullText(this.result, column.position, row); } finally { - this.activeElement = null; + this.updateCache(element, { loading: false }); } }); - return url; + if (fullText === null) { + throw new Error('Failed to get value metadata information'); + } + + this.updateCache(element, { fullText }); + + return fullText; } async resolveFileDataUrl(element: IResultSetElementKey) { - const cache = this.retrieveFileDataUrlFromCache(element); + const cachedUrl = this.retrieveBlobFromCache(element); - if (cache) { - return cache; + if (cachedUrl) { + return cachedUrl; } const url = await this.getFileDataUrl(element); - this.cache.set(this.getHash(element), url); + const blob = await downloadFromURL(url); - return url; - } + this.updateCache(element, { blob }); - retrieveFileDataUrlFromCache(element: IResultSetElementKey) { - const hash = this.getHash(element); - return this.cache.get(hash); + return blob; } async downloadFileData(element: IResultSetElementKey) { @@ -117,24 +179,78 @@ export class ResultSetDataContentAction extends DatabaseDataAction { + const column = this.data.getColumn(element.column); + const row = this.data.getRowValue(element.row); + + if (!row || !column) { + throw new Error('Failed to get value metadata information'); + } + + const url = await this.source.runOperation(async () => { + try { + this.updateCache(element, { loading: true }); + return await this.loadDataURL(this.result, column.position, row); + } finally { + this.updateCache(element, { loading: false }); + } + }); + + if (url === null) { + throw new Error('Failed to get value metadata information'); + } + + return url; + } + + private async loadFileFullText(result: IDatabaseResultSet, columnIndex: number, row: IResultSetValue[]) { + if (!result.id) { + throw new Error("Result's id must be provided"); + } + + const response = await this.graphQLService.sdk.sqlReadStringValue({ + resultsId: result.id, + projectId: result.projectId, + connectionId: result.connectionId, + contextId: result.contextId, + columnIndex, + row: { + data: row, + }, + }); + + return response.text; + } + + private updateCache(element: IResultSetElementKey, partialCache: Partial) { + const cachedElement = this.getCache(element) ?? {}; + this.setCache(element, { ...cachedElement, ...partialCache }); } - private generateFileDataUrl(fileName: string) { - return `${GlobalConstants.serviceURI}/${RESULT_VALUE_PATH}/${fileName}`; + private getCache(element: IResultSetElementKey) { + return this.cache.get(element, CONTENT_CACHE_KEY); } - private getHash(element: IResultSetElementKey) { - return ResultSetDataKeysUtils.serializeElementKey(element); + private setCache(element: IResultSetElementKey, value: ICacheEntry) { + this.cache.set(element, CONTENT_CACHE_KEY, value); } - private async loadFileName(result: IDatabaseResultSet, columnIndex: number, row: IResultSetValue[]) { + private async loadDataURL(result: IDatabaseResultSet, columnIndex: number, row: IResultSetValue[]) { if (!result.id) { throw new Error("Result's id must be provided"); } - const response = await this.graphQLService.sdk.getResultsetDataURL({ + const { url } = await this.graphQLService.sdk.getResultsetDataURL({ resultsId: result.id, + projectId: result.projectId, connectionId: result.connectionId, contextId: result.contextId, lobColumnIndex: columnIndex, @@ -143,6 +259,6 @@ export class ResultSetDataContentAction extends DatabaseDataAction(a: T, b: T): boolean { + isEqual(a: T, b: T): boolean { if (a.index !== b.index) { return false; } - const keyA = 'key' in a; - const keyB = 'key' in b; + const keyA = 'subIndex' in a; + const keyB = 'subIndex' in b; if (keyA !== keyB) { return false; } - if (keyA && (a as IResultSetRowKey).key !== (b as IResultSetRowKey).key) { + if (keyA && (a as IResultSetRowKey).subIndex !== (b as IResultSetRowKey).subIndex) { return false; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts index 6b05aba33b..63b1adcaab 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.ts @@ -1,32 +1,38 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, computed, makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; -import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import { ResultDataFormat, SqlResultRow, UpdateResultsDataBatchMutationVariables } from '@cloudbeaver/core-sdk'; -import { uuid } from '@cloudbeaver/core-utils'; +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { ResultDataFormat, type SqlResultRow, type AsyncUpdateResultsDataBatchMutationVariables } from '@cloudbeaver/core-sdk'; +import { isNull } from '@cloudbeaver/core-utils'; +import { isResultSetContentValue, isResultSetComplexValue } from '@dbeaver/result-set-api'; -import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; -import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; -import { databaseDataAction } from '../DatabaseDataActionDecorator'; -import { DatabaseEditAction } from '../DatabaseEditAction'; +import type { IDatabaseDataSource } from '../../IDatabaseDataSource.js'; +import type { IDatabaseResultSet } from '../../IDatabaseResultSet.js'; +import { databaseDataAction } from '../DatabaseDataActionDecorator.js'; +import { DatabaseEditAction } from '../DatabaseEditAction.js'; import { DatabaseEditChangeType, - IDatabaseDataEditActionData, - IDatabaseDataEditActionValue, - IDatabaseDataEditApplyActionData, - IDatabaseDataEditApplyActionUpdate, -} from '../IDatabaseDataEditAction'; -import type { IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey'; -import { isResultSetContentValue } from './isResultSetContentValue'; -import { ResultSetDataAction } from './ResultSetDataAction'; -import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils'; -import type { IResultSetValue } from './ResultSetFormatAction'; + type IDatabaseDataEditActionData, + type IDatabaseDataEditActionValue, + type IDatabaseDataEditApplyActionData, + type IDatabaseDataEditApplyActionUpdate, +} from '../IDatabaseDataEditAction.js'; +import { compareResultSetRowKeys } from './compareResultSetRowKeys.js'; +import { createResultSetContentValue } from './createResultSetContentValue.js'; +import { createResultSetFileValue } from './createResultSetFileValue.js'; +import type { IResultSetBlobValue } from './IResultSetBlobValue.js'; +import type { IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey.js'; +import { isResultSetBlobValue } from './isResultSetBlobValue.js'; +import { isResultSetFileValue } from './isResultSetFileValue.js'; +import { ResultSetDataAction } from './ResultSetDataAction.js'; +import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils.js'; +import type { IResultSetValue } from './ResultSetFormatAction.js'; export interface IResultSetUpdate { row: IResultSetRowKey; @@ -39,9 +45,9 @@ export type IResultSetEditActionData = IDatabaseDataEditActionData { - static dataFormat = [ResultDataFormat.Resultset]; + static override dataFormat = [ResultDataFormat.Resultset]; - readonly applyAction: ISyncExecutor>; + override readonly applyAction: ISyncExecutor>; private readonly editorData: Map; private readonly data: ResultSetDataAction; @@ -54,8 +60,6 @@ export class ResultSetEditAction extends DatabaseEditAction(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; @@ -210,32 +205,32 @@ export class ResultSetEditAction extends DatabaseEditAction(); for (const key of keys) { const serialized = ResultSetDataKeysUtils.serialize(key.row); if (!rowKeys.has(serialized)) { - rows.push(key.row); + result.push(key); rowKeys.add(serialized); } } - this.duplicateRow(...rows); + this.duplicateRow(...result); } - duplicateRow(...rows: IResultSetRowKey[]): void { - for (const row of rows) { - let value = this.data.getRowValue(row); + duplicateRow(...keys: IResultSetElementKey[]): void { + for (const key of keys) { + let value = this.data.getRowValue(key.row); - const editedValue = this.editorData.get(ResultSetDataKeysUtils.serialize(row)); + const editedValue = this.editorData.get(ResultSetDataKeysUtils.serialize(key.row)); if (editedValue) { value = editedValue.update; } - this.addRow(row, JSON.parse(JSON.stringify(value))); + this.addRow(key.row, JSON.parse(JSON.stringify(value)), key.column); } } @@ -279,6 +274,10 @@ export class ResultSetEditAction extends DatabaseEditAction> = []; - let rowIndex = 0; - let addShift = 0; - let deleteShift = 0; - const insertedRows: IResultSetRowKey[] = []; + 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)); - if (result.data?.rows?.length !== this.updates.length) { - console.warn('ResultSetEditAction: returned data differs from performed update'); - } + 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 this.updates) { - switch (update.type) { + for (const update of tempUpdates) { + const value = result.data?.rowsWithMetaData?.[update.rowIndex]?.data; + 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 { + fillBatch(batch: AsyncUpdateResultsDataBatchMutationVariables): void { for (const update of this.updates) { switch (update.type) { case DatabaseEditChangeType.update: { @@ -479,11 +489,16 @@ 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; }, {}), + metaData: this.data.getRowMetadata(update.row), }); } break; @@ -495,7 +510,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..ca9acbb4fb 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 @@ -1,25 +1,34 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { removeLineBreak } from '@cloudbeaver/core-utils'; +import { isResultSetContentValue, isResultSetComplexValue, type IResultSetComplexValue } from '@dbeaver/result-set-api'; -import { DatabaseDataAction } from '../../DatabaseDataAction'; -import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; -import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; -import { databaseDataAction } from '../DatabaseDataActionDecorator'; -import { DatabaseEditChangeType } from '../IDatabaseDataEditAction'; -import type { IDatabaseDataFormatAction } from '../IDatabaseDataFormatAction'; -import type { IResultSetElementKey, IResultSetPartialKey } from './IResultSetDataKey'; -import { isResultSetContentValue } from './isResultSetContentValue'; -import { ResultSetEditAction } from './ResultSetEditAction'; -import { ResultSetViewAction } from './ResultSetViewAction'; +import { DatabaseDataAction } from '../../DatabaseDataAction.js'; +import type { IDatabaseDataSource } from '../../IDatabaseDataSource.js'; +import type { IDatabaseResultSet } from '../../IDatabaseResultSet.js'; +import { databaseDataAction } from '../DatabaseDataActionDecorator.js'; +import { DatabaseEditChangeType } from '../IDatabaseDataEditAction.js'; +import type { IDatabaseDataFormatAction } from '../IDatabaseDataFormatAction.js'; +import type { IResultSetColumnKey, IResultSetElementKey, IResultSetPartialKey } from './IResultSetDataKey.js'; +import { isResultSetFileValue } from './isResultSetFileValue.js'; +import { isResultSetGeometryValue } from './isResultSetGeometryValue.js'; +import { ResultSetEditAction } from './ResultSetEditAction.js'; +import { ResultSetViewAction } from './ResultSetViewAction.js'; -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,74 +46,152 @@ export class ResultSetFormatAction this.edit = edit; } - getHeaders(): string[] { - return this.view.columns.map(column => column.name!).filter(name => name !== undefined); + isReadOnly(key: IResultSetPartialKey): boolean { + let readonly = false; + + if (key.column) { + readonly = this.view.getColumn(key.column)?.readOnly || false; + } + + if (key.column && key.row) { + if (!readonly) { + readonly = this.edit.getElementState(key as IResultSetElementKey) === DatabaseEditChangeType.delete; + } + } + + return readonly; + } + isNull(key: IResultSetElementKey): boolean { + return this.get(key) === null; } - getLongestCells(offset = 0, count?: number): string[] { - const rows = this.view.rows.slice(offset, count); - const cells: string[] = []; + isBinary(key: IResultSetPartialKey): boolean { + if (!key.column) { + return false; + } - 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] ?? ''; + const column = this.view.getColumn(key.column); + if (column?.dataKind?.toLocaleLowerCase() === 'binary') { + return true; + } - if (value.length > current.length) { - cells[columnIndex] = value; - } + if (key.row) { + const value = this.get(key as IResultSetElementKey); + + if (isResultSetFileValue(value)) { + return true; + } + + if (isResultSetContentValue(value)) { + return value.binary !== undefined; } } - return cells; + return false; } - isReadOnly(key: IResultSetPartialKey): boolean { - let readonly = false; - + isGeometry(key: IResultSetPartialKey) { if (key.column) { - readonly = this.view.getColumn(key.column)?.readOnly || false; + const column = this.view.getColumn(key.column); + if (column?.dataKind?.toLocaleLowerCase() === 'geometry') { + return true; + } } - if (key.column && key.row) { - if (!readonly) { - readonly = this.edit.getElementState(key as IResultSetElementKey) === DatabaseEditChangeType.delete; + if (key.row) { + const value = this.get(key as IResultSetElementKey); + return isResultSetComplexValue(value) && value.$type === 'geometry'; + } + + return false; + } + + isText(key: IResultSetPartialKey): boolean { + if (!key?.column) { + return false; + } + + const column = this.view.getColumn(key.column); + + if (column?.dataKind?.toLocaleLowerCase() === 'string') { + return true; + } + + if (key.row && !this.isBinary(key)) { + const value = this.get(key as IResultSetElementKey); + + if (isResultSetContentValue(value)) { + return value.text !== undefined; } + } - if (!readonly) { - const value = this.view.getCellValue(key as IResultSetElementKey); + return false; + } - if (isResultSetContentValue(value)) { - readonly = value.binary !== undefined || value.contentLength !== value.text?.length; - } else if (value !== null && typeof value === 'object') { - readonly = true; + getHeaders(): string[] { + return this.view.columns.map(column => column?.name).filter((name): name is string => name !== undefined && name !== null); + } + + getLongestCells(column?: IResultSetColumnKey, offset = 0, count?: number): string[] { + const cells: string[] = []; + const columns = column ? [column] : this.view.columnKeys; + count ??= this.view.rowKeys.length; + + for (let rowIndex = offset; rowIndex < offset + count; rowIndex++) { + for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) { + const key = { row: this.view.rowKeys[rowIndex]!, column: columns[columnIndex]! }; + const displayString = this.getDisplayString(key); + const current = cells[columnIndex] ?? ''; + + if (displayString.length > current.length) { + cells[columnIndex] = displayString; } } } - return readonly; + return cells; } - isNull(value: IResultSetValue): boolean { - return this.get(value) === null; + get(key: IResultSetElementKey): IResultSetValue { + return this.view.getCellValue(key); } - get(value: IResultSetValue): IResultSetValue { - if (value !== null && typeof value === 'object') { - if ('text' in value) { + getText(key: IResultSetElementKey): string { + const value = this.get(key); + + if (value === null) { + return ''; + } + + if (isResultSetContentValue(value)) { + if (value.text !== undefined) { return value.text; - } else if ('value' in value) { - return value.value; } - return value; + + return ''; } - return value; - } + if (isResultSetGeometryValue(value)) { + if (value.text !== undefined) { + return value.text; + } + + return ''; + } - getText(value: IResultSetValue): string | null { - value = this.get(value); + 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 +204,57 @@ 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)) { + if (isResultSetContentValue(value) && value.text === 'null') { + return '[null]'; + } + + return '[blob]'; } - return removeLineBreak(String(value)); + 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 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..87da028693 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 @@ -1,37 +1,37 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, computed, IReactionDisposer, makeObservable, observable, reaction, toJS } from 'mobx'; +import { action, computed, type IReactionDisposer, makeObservable, observable, reaction, toJS } from 'mobx'; -import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; -import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; -import { databaseDataAction } from '../DatabaseDataActionDecorator'; -import { DatabaseSelectAction } from '../DatabaseSelectAction'; -import { DatabaseEditChangeType, IDatabaseDataEditActionData, IDatabaseDataEditApplyActionData } from '../IDatabaseDataEditAction'; -import type { DatabaseDataSelectActionsData } from '../IDatabaseDataSelectAction'; -import type { IResultSetColumnKey, IResultSetElementKey, IResultSetPartialKey, IResultSetRowKey } from './IResultSetDataKey'; -import { ResultSetDataAction } from './ResultSetDataAction'; -import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils'; -import { ResultSetEditAction } from './ResultSetEditAction'; -import type { IResultSetValue } from './ResultSetFormatAction'; -import { ResultSetViewAction } from './ResultSetViewAction'; +import type { IDatabaseDataSource } from '../../IDatabaseDataSource.js'; +import type { IDatabaseResultSet } from '../../IDatabaseResultSet.js'; +import { databaseDataAction } from '../DatabaseDataActionDecorator.js'; +import { DatabaseSelectAction } from '../DatabaseSelectAction.js'; +import { DatabaseEditChangeType, type IDatabaseDataEditActionData, type IDatabaseDataEditApplyActionData } from '../IDatabaseDataEditAction.js'; +import type { DatabaseDataSelectActionsData } from '../IDatabaseDataSelectAction.js'; +import type { IResultSetColumnKey, IResultSetElementKey, IResultSetPartialKey, IResultSetRowKey } from './IResultSetDataKey.js'; +import { ResultSetDataAction } from './ResultSetDataAction.js'; +import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils.js'; +import { ResultSetEditAction } from './ResultSetEditAction.js'; +import type { IResultSetValue } from './ResultSetFormatAction.js'; +import { ResultSetViewAction } from './ResultSetViewAction.js'; @databaseDataAction() export class ResultSetSelectAction extends DatabaseSelectAction { - static dataFormat = [ResultDataFormat.Resultset]; + static override dataFormat = [ResultDataFormat.Resultset]; get elements(): IResultSetElementKey[] { return Array.from(this.selectedElements.values()).flat(); } - readonly actions: ISyncExecutor>; + override readonly actions: ISyncExecutor>; readonly selectedElements: Map; private focusedElement: IResultSetElementKey | null; @@ -78,7 +78,7 @@ export class ResultSetSelectAction extends DatabaseSelectAction ResultSetDataKeysUtils.isEqual(key, focus.row))) { for (let index = focusIndex; index >= 0; index--) { - const previousElement = previous[index]; + const previousElement = previous[index]!; const row = current.find(key => ResultSetDataKeysUtils.isEqual(key, previousElement)); if (row) { @@ -87,7 +87,7 @@ export class ResultSetSelectAction extends DatabaseSelectAction ResultSetDataKeysUtils.isEqual(key, nextElement)); if (row) { @@ -96,7 +96,7 @@ export class ResultSetSelectAction extends DatabaseSelectAction 0) { - this.focus(data.value[data.value.length - 1].key); + this.focus(data.value[data.value.length - 1]!.key); } this.clear(); } @@ -357,13 +361,13 @@ export class ResultSetSelectAction extends DatabaseSelectAction 0) { - this.focus(data.value[0].key); + this.focus(data.value[0]!.key); this.clear(); } break; case DatabaseEditChangeType.update: if (data.value && data.value.length > 0) { - this.focus(data.value[data.value.length - 1].key); + this.focus(data.value[data.value.length - 1]!.key); } break; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts index 6584a4ed5c..447c569701 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts @@ -1,45 +1,45 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, computed, makeObservable, observable } from 'mobx'; -import { DataTypeLogicalOperation, ResultDataFormat, SqlResultColumn } from '@cloudbeaver/core-sdk'; - -import { DatabaseDataAction } from '../../DatabaseDataAction'; -import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; -import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; -import { databaseDataAction } from '../DatabaseDataActionDecorator'; -import type { IDatabaseDataResultAction } from '../IDatabaseDataResultAction'; -import type { IResultSetContentValue } from './IResultSetContentValue'; -import type { IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey'; -import { isResultSetContentValue } from './isResultSetContentValue'; -import { ResultSetDataAction } from './ResultSetDataAction'; -import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils'; -import { ResultSetEditAction } from './ResultSetEditAction'; -import type { IResultSetValue } from './ResultSetFormatAction'; +import { type DataTypeLogicalOperation, ResultDataFormat, type SqlResultColumn } from '@cloudbeaver/core-sdk'; +import { type IResultSetComplexValue, isResultSetContentValue } from '@dbeaver/result-set-api'; + +import { DatabaseDataAction } from '../../DatabaseDataAction.js'; +import type { IDatabaseDataAction } from '../../IDatabaseDataAction.js'; +import type { IDatabaseDataSource } from '../../IDatabaseDataSource.js'; +import type { IDatabaseResultSet } from '../../IDatabaseResultSet.js'; +import { databaseDataAction } from '../DatabaseDataActionDecorator.js'; +import { compareResultSetRowKeys } from './compareResultSetRowKeys.js'; +import type { IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey.js'; +import { ResultSetDataAction } from './ResultSetDataAction.js'; +import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils.js'; +import { ResultSetEditAction } from './ResultSetEditAction.js'; +import type { IResultSetValue } from './ResultSetFormatAction.js'; @databaseDataAction() -export class ResultSetViewAction extends DatabaseDataAction implements IDatabaseDataResultAction { +export class ResultSetViewAction extends DatabaseDataAction implements IDatabaseDataAction { static dataFormat = [ResultDataFormat.Resultset]; get rowKeys(): IResultSetRowKey[] { - return [...this.editor.addRows, ...this.data.rows.map((c, index) => ({ 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[] { - return this.columns.map(c => ({ index: this.data.columns.indexOf(c) })); + return this.columnsOrder.map(index => ({ index })); } get rows(): IResultSetValue[][] { - return this.data.rows; + return this.data.rows.map(row => row.data || []); } get columns(): SqlResultColumn[] { - return this.columnsOrder.map(i => this.data.columns[i]); + return this.columnsOrder.map(i => this.data.columns[i]).filter(column => column !== undefined); } private columnsOrder: number[] = []; @@ -52,12 +52,12 @@ export class ResultSetViewAction extends DatabaseDataAction(this, { - rowKeys: computed, - columnKeys: computed, - rows: computed, - columns: computed, columnsOrder: observable, setColumnOrder: action, + rows: computed, + rowKeys: computed, + columns: computed, + columnKeys: computed, }); } @@ -129,7 +129,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]; + 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)) { @@ -158,7 +158,13 @@ export class ResultSetViewAction extends DatabaseDataAction operation.argumentCount === 1 || operation.argumentCount === 0); } - updateResult(result: IDatabaseResultSet, index: number): void { + override updateResult(result: IDatabaseResultSet, index: number): void { super.updateResult(result, index); if (this.columnsOrder.length !== this.data.columns.length) { this.columnsOrder = this.data.columns.map((key, index) => index); 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..cfd6bb079e --- /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-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IResultSetRowKey } from './IResultSetDataKey.js'; + +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..347f83a172 --- /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-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createResultSetFileValue } from './createResultSetFileValue.js'; +import type { IResultSetBlobValue } from './IResultSetBlobValue.js'; + +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..7a0ce41bf7 --- /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-2025 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 '@dbeaver/result-set-api'; + +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..606e18d1f1 --- /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-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IResultSetFileValue } from './IResultSetFileValue.js'; + +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..c8e8d8f1eb --- /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-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IResultSetBlobValue } from './IResultSetBlobValue.js'; +import { isResultSetFileValue } from './isResultSetFileValue.js'; + +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/isResultSetContentValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts deleted file mode 100644 index 38303410d7..0000000000 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts +++ /dev/null @@ -1,12 +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 type { IResultSetContentValue } from './IResultSetContentValue'; - -export function isResultSetContentValue(value: any): value is IResultSetContentValue { - return value !== null && typeof value === 'object' && '$type' in 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..61d9c22aa1 --- /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-2025 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 { isResultSetComplexValue } from '@dbeaver/result-set-api'; +import type { IResultSetFileValue } from './IResultSetFileValue.js'; + +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..57830c18dc --- /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-2025 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 { isResultSetComplexValue } from '@dbeaver/result-set-api'; +import type { IResultSetGeometryValue } from './IResultSetGeometryValue.js'; + +export function isResultSetGeometryValue(value: any): value is IResultSetGeometryValue { + return isResultSetComplexValue(value) && value.$type === 'geometry'; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.ts index 95c4a6d980..dd50ab5d52 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.ts @@ -1,13 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { createDataContext } from '@cloudbeaver/core-data-context'; -import type { IDatabaseDataModel } from '../IDatabaseDataModel'; -import type { IDatabaseDataOptions } from '../IDatabaseDataOptions'; +import type { IDatabaseDataModel } from '../IDatabaseDataModel.js'; -export const DATA_CONTEXT_DV_DDM = createDataContext>('data-viewer-database-data-model'); +export const DATA_CONTEXT_DV_DDM = createDataContext('data-viewer-database-data-model'); diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.ts index 29f1a73916..5daed98ede 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DataContext/DATA_CONTEXT_DV_PRESENTATION.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DataContext/DATA_CONTEXT_DV_PRESENTATION.ts new file mode 100644 index 0000000000..ed0e6fd3f2 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DataContext/DATA_CONTEXT_DV_PRESENTATION.ts @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createDataContext } from '@cloudbeaver/core-data-context'; + +export enum DataViewerPresentationType { + Data, + Analytical, +} + +export interface IDataViewerPresentation { + type?: DataViewerPresentationType; + readonly?: boolean; +} + +export const DATA_CONTEXT_DV_PRESENTATION = createDataContext('data-viewer-presentation'); diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataAction.ts index 07e228eeb9..3f58761181 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataAction.ts @@ -1,24 +1,20 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { makeObservable, observable } from 'mobx'; -import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction'; -import type { IDatabaseDataResult } from './IDatabaseDataResult'; -import type { IDatabaseDataSource } from './IDatabaseDataSource'; +import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction.js'; +import type { IDatabaseDataResult } from './IDatabaseDataResult.js'; +import type { IDatabaseDataSource } from './IDatabaseDataSource.js'; export abstract class DatabaseDataAction implements IDatabaseDataAction { result!: TResult; resultIndex: number; - get empty(): boolean { - return !this.result.data; - } - readonly source: IDatabaseDataSource; constructor(source: IDatabaseDataSource) { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts index 2de6937126..2b1a16cdc9 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts @@ -1,20 +1,21 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { action, makeObservable, runInAction } from 'mobx'; +import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { MetadataMap } from '@cloudbeaver/core-utils'; -import { getDependingDataActions } from './Actions/DatabaseDataActionDecorator'; -import { isDatabaseDataAction } from './DatabaseDataAction'; -import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction'; -import type { IDatabaseDataActions } from './IDatabaseDataActions'; -import type { IDatabaseDataResult } from './IDatabaseDataResult'; -import type { IDatabaseDataSource } from './IDatabaseDataSource'; +import { getDependingDataActions } from './Actions/DatabaseDataActionDecorator.js'; +import { isDatabaseDataAction } from './DatabaseDataAction.js'; +import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction.js'; +import type { IDatabaseDataActions } from './IDatabaseDataActions.js'; +import type { IDatabaseDataResult } from './IDatabaseDataResult.js'; +import type { IDatabaseDataSource } from './IDatabaseDataSource.js'; type ActionsList = Array>; @@ -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,9 +57,9 @@ 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)); + depends.push(this.source.serviceProvider.getService(dependency as any)); } } @@ -68,7 +69,8 @@ export class DatabaseDataActions action = new Action(this.source, ...depends); action.updateResult(result, this.source.results.indexOf(result)); - this.actions.set(result.uniqueResultId, [...actions, action]); + action.afterResultUpdate(); + this.actions.set(result.uniqueResultId, [...this.actions.get(result.uniqueResultId), action]); }); } @@ -80,7 +82,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 +94,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 +117,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/DatabaseDataModel/DatabaseDataFormat.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataFormat.ts index 47e1dd344a..74aaf6152d 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataFormat.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataFormat.ts @@ -1,12 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { DatabaseDataModel } from './DatabaseDataModel'; +import type { DatabaseDataModel } from './DatabaseDataModel.js'; export interface DatabaseDataFormat { - processResult: (model: DatabaseDataModel, data: T) => void; + processResult: (model: DatabaseDataModel, data: T) => void; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts index 0c0deb2a4e..1b101f2cf8 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts @@ -1,24 +1,23 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { makeObservable, observable } from 'mobx'; -import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-executor'; +import { Executor, ExecutorInterrupter, type IExecutor } from '@cloudbeaver/core-executor'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { uuid } from '@cloudbeaver/core-utils'; -import type { IDatabaseDataModel, IRequestEventData } from './IDatabaseDataModel'; -import type { IDatabaseDataResult } from './IDatabaseDataResult'; -import type { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; +import type { IDatabaseDataModel, IRequestEventData } from './IDatabaseDataModel.js'; +import type { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource.js'; -export class DatabaseDataModel implements IDatabaseDataModel { +export class DatabaseDataModel = IDatabaseDataSource> implements IDatabaseDataModel { id: string; name: string | null; - source: IDatabaseDataSource; + source: TSource; countGain: number; get requestInfo(): IRequestInfo { @@ -31,11 +30,9 @@ export class DatabaseDataModel>; + readonly onRequest: IExecutor>; - private currentTask: Promise | null; - - constructor(source: IDatabaseDataSource) { + constructor(source: TSource) { this.id = uuid(); this.name = null; this.source = source; @@ -43,7 +40,7 @@ export class DatabaseDataModel ({ ...data, model: this })); makeObservable(this, { countGain: observable, @@ -54,7 +51,7 @@ export class DatabaseDataModel= count; - } - - getResults(): TResult[] { - return this.source.results; + hasElementIdentifier(resultIndex: number): boolean { + return this.source.hasElementIdentifier(resultIndex); } - getResult(index: number): TResult | null { - return this.source.getResult(index); + isDataAvailable(offset: number, count: number): boolean { + return this.source.isDataAvailable(offset, count); } setName(name: string | null) { @@ -79,11 +72,6 @@ export class DatabaseDataModel { const contexts = await this.onOptionsChange.execute(); @@ -121,41 +104,33 @@ export class DatabaseDataModel { - await this.requestSaveAction(() => this.source.saveData()); + await this.source.saveData(); } async retry(): Promise { - await this.requestDataAction(() => this.source.retry()); + await this.source.retry(); } - async refresh(concurrent?: boolean): Promise { - if (concurrent) { - await this.source.refreshData(); - return; - } - await this.requestDataAction(() => this.source.refreshData()); + async refresh(): Promise { + await this.source.refreshData(); } - async request(concurrent?: boolean): Promise { - if (concurrent) { - await this.source.requestData(); - return; - } - await this.requestDataAction(() => this.source.requestData()); + async request(mutation?: () => void): Promise { + await this.source.requestData(mutation); } async reload(): Promise { - await this.requestDataAction(() => this.source.setSlice(0, this.countGain).requestData()); + await this.request(() => { + this.setSlice(0, this.countGain); + }); } async requestDataPortion(offset: number, count: number): Promise { - if (!this.isDataAvailable(offset, count)) { - await this.requestDataAction(() => this.source.setSlice(offset, count).requestData()); - } + await this.source.requestDataPortion(offset, count); } - cancel(): Promise | void { - return this.source.cancel(); + async cancel(): Promise { + await this.source.cancel(); } resetData(): void { @@ -166,38 +141,4 @@ export class DatabaseDataModel Promise | void): Promise { - return action(); - } - - async requestDataAction(action: () => Promise | void): Promise { - if (this.currentTask) { - return this.currentTask; - } - - try { - this.currentTask = this.requestDataActionTask(action); - return await this.currentTask; - } finally { - this.currentTask = null; - } - } - - private async requestDataActionTask(action: () => Promise | void): Promise { - let contexts = await this.onRequest.execute({ type: 'on', model: this }); - - if (ExecutorInterrupter.isInterrupted(contexts)) { - return; - } - - contexts = await this.onRequest.execute({ type: 'before', model: this }); - - if (ExecutorInterrupter.isInterrupted(contexts)) { - return; - } - - await action(); - await this.onRequest.execute({ type: 'after', model: this }); - } } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index e966da5817..2b3ed3c5ea 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -1,21 +1,27 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, makeObservable, observable, toJS } from 'mobx'; -import type { IConnectionExecutionContext } from '@cloudbeaver/core-connections'; -import type { IServiceInjector } from '@cloudbeaver/core-di'; +import type { IServiceProvider } from '@cloudbeaver/core-di'; +import { Executor, ExecutorInterrupter, type IExecutor, type ITask, Task } from '@cloudbeaver/core-executor'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { DatabaseDataActions } from './DatabaseDataActions'; -import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction'; -import type { IDatabaseDataActions } from './IDatabaseDataActions'; -import type { IDatabaseDataResult } from './IDatabaseDataResult'; -import { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; +import { DatabaseDataActions } from './DatabaseDataActions.js'; +import type { IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction.js'; +import type { IDatabaseDataActions } from './IDatabaseDataActions.js'; +import type { IDatabaseDataResult } from './IDatabaseDataResult.js'; +import { + DatabaseDataAccessMode, + DatabaseDataSourceOperation, + type IDatabaseDataSource, + type IDatabaseDataSourceOperationEvent, + type IRequestInfo, +} from './IDatabaseDataSource.js'; export abstract class DatabaseDataSource implements IDatabaseDataSource { access: DatabaseDataAccessMode; @@ -30,35 +36,48 @@ export abstract class DatabaseDataSource | null; - private activeSave: Promise | null; - private activeTask: Promise | null; private lastAction: () => Promise; + private outdated: boolean; - constructor(serviceInjector: IServiceInjector) { - this.serviceInjector = serviceInjector; + readonly onOperation: IExecutor; + private get activeOperation(): Promise | null { + return this.activeOperationStack[this.activeOperationStack.length - 1] ?? null; + } + private readonly activeOperationStack: Array>; + + constructor(serviceProvider: IServiceProvider) { + this.serviceProvider = serviceProvider; this.actions = new DatabaseDataActions(this); this.access = DatabaseDataAccessMode.Default; this.results = []; + this.activeOperationStack = []; this.offset = 0; this.count = 0; this.prevOptions = null; this.options = null; this.disabled = false; - this.outdated = false; + this.outdated = true; this.constraintsAvailable = true; - this.activeRequest = null; - this.activeSave = null; - this.activeTask = null; - this.executionContext = null; + this.onOperation = new Executor(); this.dataFormat = ResultDataFormat.Resultset; this.supportedDataFormats = []; this.requestInfo = { @@ -71,7 +90,7 @@ export abstract class DatabaseDataSource, 'activeRequest' | 'activeSave' | 'activeTask' | 'disabled'>(this, { + makeObservable, 'disabled' | 'activeOperationStack' | 'outdated'>(this, { access: observable, dataFormat: observable, supportedDataFormats: observable, @@ -82,80 +101,66 @@ export abstract class DatabaseDataSource>( - resultIndex: number, - action: IDatabaseDataActionClass, - ): T | undefined; - tryGetAction>( - result: TResult, - action: IDatabaseDataActionClass, - ): T | undefined; - tryGetAction>( - resultIndex: number | TResult, - action: IDatabaseDataActionClass, - ): T | undefined { + tryGetAction>(resultIndex: number, action: T): InstanceType | undefined; + tryGetAction>(result: TResult, action: T): InstanceType | undefined; + tryGetAction>(resultIndex: number | TResult, action: T): InstanceType | undefined { if (typeof resultIndex === 'number') { if (!this.hasResult(resultIndex)) { return undefined; } - return this.actions.tryGet(this.results[resultIndex], action); + return this.actions.tryGet(this.results[resultIndex]!, action); } return this.actions.tryGet(resultIndex, action); } - getAction>(resultIndex: number, action: IDatabaseDataActionClass): T; - getAction>(result: TResult, action: IDatabaseDataActionClass): T; - getAction>( - resultIndex: number | TResult, - action: IDatabaseDataActionClass, - ): T { + getAction>(resultIndex: number, action: T): InstanceType; + getAction>(result: TResult, action: T): InstanceType; + getAction>(resultIndex: number | TResult, action: T): InstanceType { if (typeof resultIndex === 'number') { if (!this.hasResult(resultIndex)) { throw new Error('Result index out of range'); } - return this.actions.get(this.results[resultIndex], action); + return this.actions.get(this.results[resultIndex]!, action); } return this.actions.get(resultIndex, action); } - getActionImplementation>( + getActionImplementation>( resultIndex: number, - action: IDatabaseDataActionInterface, - ): T | undefined; - getActionImplementation>( - result: TResult, - action: IDatabaseDataActionInterface, - ): T | undefined; - getActionImplementation>( + action: T, + ): InstanceType | undefined; + getActionImplementation>(result: TResult, action: T): InstanceType | undefined; + getActionImplementation>( resultIndex: number | TResult, - action: IDatabaseDataActionInterface, - ): T | undefined { + action: T, + ): InstanceType | undefined { if (typeof resultIndex === 'number') { if (!this.hasResult(resultIndex)) { return undefined; } - return this.actions.getImplementation(this.results[resultIndex], action); + return this.actions.getImplementation(this.results[resultIndex]!, action); } return this.actions.getImplementation(resultIndex, action); } - abstract cancel(): Promise | void; + async cancel() { + if (this.activeOperation instanceof Task) { + await this.activeOperation.cancel(); + } + } hasResult(resultIndex: number): boolean { return resultIndex < this.results.length; @@ -163,12 +168,16 @@ export abstract class DatabaseDataSource index) { - return this.results[index]; + return this.results[index]!; } return null; } + getResults(): TResult[] { + return this.results; + } + setOutdated(): this { this.outdated = true; return this; @@ -181,20 +190,39 @@ export abstract class DatabaseDataSource= count; + } + isLoadable(): boolean { return !this.isLoading() && !this.disabled; } + hasElementIdentifier(resultIndex: number): boolean { + return this.getResult(resultIndex)?.data?.hasRowIdentifier === true; + } + isReadonly(resultIndex: number): boolean { - return this.access === DatabaseDataAccessMode.Readonly || this.results.length > 1 || !this.executionContext?.context || this.disabled; + return this.access === DatabaseDataAccessMode.Readonly || this.results.length > 1 || this.disabled; } isLoading(): boolean { - return !!this.activeRequest || !!this.activeSave || !!this.activeTask; + return !!this.activeOperation; } - isDisabled(resultIndex: number): boolean { - return this.isLoading() || this.disabled; + isDisabled(resultIndex?: number): boolean { + if (resultIndex === undefined) { + return !this.results.length && this.error === null; + } + return this.hasResult(resultIndex) && !this.getResult(resultIndex)?.data && this.error === null; } setAccess(access: DatabaseDataAccessMode): this { @@ -222,7 +250,7 @@ export abstract class DatabaseDataSource(task: () => Promise): Promise { - if (this.activeTask) { - try { - await this.activeTask; - } catch {} - } - - if (this.activeSave) { - try { - await this.activeSave; - } catch {} - } - - if (this.activeRequest) { - try { - await this.activeRequest; - } catch {} - } - - this.activeTask = task(); - - try { - return await this.activeTask; - } finally { - this.activeTask = null; - } + async runOperation(task: () => Promise): Promise { + return this.tryExecuteOperation(DatabaseDataSourceOperation.Task, task); } - async requestData(): Promise { - if (this.activeSave) { - try { - await this.activeSave; - } finally { - } - } - - if (this.activeRequest) { - await this.activeRequest; - return; - } - this.lastAction = this.requestData.bind(this); + async requestData(mutation?: () => void): Promise { + await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, () => { + this.lastAction = this.requestData.bind(this); - try { - this.activeRequest = this.requestDataAction(); + mutation?.(); + return this.requestDataAction(); + }); + } - const data = await this.activeRequest; - this.outdated = false; + async requestDataPortion(offset: number, count: number): Promise { + await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, () => { + if (!this.isDataAvailable(offset, count)) { + this.lastAction = this.requestDataPortion.bind(this, offset, count); - if (data !== null) { - this.setResults(data); + this.setSlice(offset, count); + return this.requestDataAction(); } - } finally { - this.activeRequest = null; - } + return Promise.resolve(); + }); } async refreshData(): Promise { - if (this.prevOptions) { - this.options = toJS(this.prevOptions); - } - await this.requestData(); + await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, () => { + this.lastAction = this.refreshData.bind(this); + + if (this.prevOptions) { + this.options = toJS(this.prevOptions); + } + + return this.requestDataAction(); + }); } async saveData(): Promise { - if (this.activeRequest) { - try { - await this.activeRequest; - } finally { - } - } + await this.tryExecuteOperation(DatabaseDataSourceOperation.Save, () => { + this.lastAction = this.saveData.bind(this); - if (this.activeSave) { - await this.activeSave; - return; - } - this.lastAction = this.saveData.bind(this); + return this.save(this.results).then(data => { + this.setResults(data); + //TODO: Remove this when we have virtual keys. We need to refresh the data in tables without a primary key to avoid UI glitch #5140. + if (!this.hasElementIdentifier(0)) { + this.setOutdated(); + } + }); + }); + } + async canSafelyDispose(): Promise { try { - const promise = this.save(this.results); - - if (promise instanceof Promise) { - this.activeSave = promise; - } - this.setResults(await promise); - } finally { - this.activeSave = null; + const result = await this.tryExecuteOperation(DatabaseDataSourceOperation.Request, async () => true); + return result || false; + } catch { + return false; } } @@ -342,13 +341,64 @@ export abstract class DatabaseDataSource; - abstract save(prevResults: TResult[]): Promise | TResult[]; + async dispose(): Promise { + await this.cancel(); + } - abstract dispose(): Promise; + abstract request(prevResults: TResult[]): Promise; + abstract save(prevResults: TResult[]): Promise; - async requestDataAction(): Promise { + private async requestDataAction(): Promise { this.prevOptions = toJS(this.options); - return this.request(this.results); + return this.request(this.results) + .finally(() => { + this.outdated = false; + }) + .then(data => { + if (data !== null) { + this.setResults(data); + } + return data; + }); + } + + private async tryExecuteOperation(type: DatabaseDataSourceOperation, operation: () => Promise): Promise { + if (this.activeOperation && type === DatabaseDataSourceOperation.Request) { + await this.activeOperation; + } + + const operationTask = this.executeOperation(type, operation); + try { + this.activeOperationStack.push(operationTask); + return await operationTask; + } finally { + const index = this.activeOperationStack.indexOf(operationTask); + if (index !== -1) { + this.activeOperationStack.splice(index, 1); + } + } + } + + private executeOperation(type: DatabaseDataSourceOperation, operation: () => Promise | T): ITask { + return new Task(async () => await this.onOperation.execute({ stage: 'request', operation: type })).run().then(contexts => { + // TODO: maybe it's better to throw an exception instead, so we will not have unexpected undefined results + if (ExecutorInterrupter.isInterrupted(contexts)) { + return null; + } + + return new Task(async () => await this.onOperation.execute({ stage: 'before', operation: type })) + .run() + .then(contexts => { + if (ExecutorInterrupter.isInterrupted(contexts)) { + return null; + } + + return operation(); + }) + .then(async result => { + await this.onOperation.execute({ stage: 'after', operation: type }); + return result; + }); + }); } } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataAction.ts index 8fe66c1153..1eb22b1ae9 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataAction.ts @@ -1,14 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import type { IDatabaseDataResult } from './IDatabaseDataResult'; -import type { IDatabaseDataSource } from './IDatabaseDataSource'; +import type { IDatabaseDataResult } from './IDatabaseDataResult.js'; +import type { IDatabaseDataSource } from './IDatabaseDataSource.js'; type AbstractConstructorFunction< TOptions, @@ -41,7 +41,6 @@ export type IDatabaseDataActionClass< export interface IDatabaseDataAction { readonly source: IDatabaseDataSource; - readonly empty: boolean; result: TResult; resultIndex: number; updateResult: (result: TResult, index: number) => void; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataActions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataActions.ts index 47726e4b81..716247ea3e 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataActions.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataActions.ts @@ -1,12 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction'; -import type { IDatabaseDataResult } from './IDatabaseDataResult'; +import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction.js'; +import type { IDatabaseDataResult } from './IDatabaseDataResult.js'; export interface IDatabaseDataActions { tryGet: >( diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataEditor.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataEditor.ts index e153636431..f793248faf 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataEditor.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataEditor.ts @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { IExecutor } from '@cloudbeaver/core-executor'; -import type { IDatabaseDataResult } from './IDatabaseDataResult'; +import type { IDatabaseDataResult } from './IDatabaseDataResult.js'; export enum DataUpdateType { delete, diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts index 84ce052e64..1ecb50bb1e 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,19 +8,17 @@ import type { IExecutor } from '@cloudbeaver/core-executor'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import type { IDatabaseDataResult } from './IDatabaseDataResult'; -import type { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; +import type { DatabaseDataAccessMode, IDatabaseDataSource, IDatabaseDataSourceOperationEvent, IRequestInfo } from './IDatabaseDataSource.js'; -export interface IRequestEventData { - type: 'before' | 'after' | 'on'; - model: IDatabaseDataModel; +export interface IRequestEventData = IDatabaseDataSource> extends IDatabaseDataSourceOperationEvent { + model: IDatabaseDataModel; } /** Represents an interface for interacting with a database. It is used for managing and requesting data. */ -export interface IDatabaseDataModel { +export interface IDatabaseDataModel = IDatabaseDataSource> { readonly id: string; readonly name: string | null; - readonly source: IDatabaseDataSource; + readonly source: TSource; /** Holds metadata about a data request. */ readonly requestInfo: IRequestInfo; readonly supportedDataFormats: ResultDataFormat[]; @@ -28,31 +26,27 @@ export interface IDatabaseDataModel>; + readonly onRequest: IExecutor>; readonly onDispose: IExecutor; setName: (name: string | null) => this; isReadonly: (resultIndex: number) => boolean; - isDisabled: (resultIndex: number) => boolean; + hasElementIdentifier: (resultIndex: number) => boolean; + isDisabled: (resultIndex?: number) => boolean; isLoading: () => boolean; isDataAvailable: (offset: number, count: number) => boolean; - getResults: () => TResult[]; - getResult: (index: number) => TResult | null; - setAccess: (access: DatabaseDataAccessMode) => this; setCountGain: (count: number) => this; setSlice: (offset: number, count?: number) => this; - setOptions: (options: TOptions) => this; setDataFormat: (dataFormat: ResultDataFormat) => this; setSupportedDataFormats: (dataFormats: ResultDataFormat[]) => this; requestOptionsChange: () => Promise; - requestDataAction: (action: () => Promise | void) => Promise; retry: () => Promise; save: () => Promise; - refresh: (concurrent?: boolean) => Promise; - request: (concurrent?: boolean) => Promise; + refresh: () => Promise; + request: (mutation?: () => void) => Promise; reload: () => Promise; requestDataPortion: (offset: number, count: number) => Promise; cancel: () => Promise | void; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts index 95a970d28d..08d9f6a75c 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataOptions.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataResult.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataResult.ts index f0fd9e253a..3ce35f7cf9 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataResult.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataResult.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,10 +10,8 @@ import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; export interface IDatabaseDataResult { id: string | null; uniqueResultId: string; - connectionId: string; - contextId: string; dataFormat: ResultDataFormat; loadedFully: boolean; - updateRowCount: number; + count: number; data: any; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts index 8a081b7f7c..cbd4fc739c 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -1,22 +1,36 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IConnectionExecutionContext } from '@cloudbeaver/core-connections'; -import type { IServiceInjector } from '@cloudbeaver/core-di'; +import type { IServiceProvider } from '@cloudbeaver/core-di'; +import type { IExecutor } from '@cloudbeaver/core-executor'; +import { type TLocalizationToken } from '@cloudbeaver/core-localization'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction'; -import type { IDatabaseDataActions } from './IDatabaseDataActions'; -import type { IDatabaseDataResult } from './IDatabaseDataResult'; +import type { IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction.js'; +import type { IDatabaseDataActions } from './IDatabaseDataActions.js'; +import type { IDatabaseDataResult } from './IDatabaseDataResult.js'; + +export enum DatabaseDataSourceOperation { + /** Abstract operation with data, should not lead to data lost */ + Task = 'task', + /** Saving operation */ + Save = 'save', + /** May lead to data lost */ + Request = 'request', +} +export interface IDatabaseDataSourceOperationEvent { + stage: 'request' | 'before' | 'after'; + operation: DatabaseDataSourceOperation; +} export interface IRequestInfo { readonly originalQuery: string; readonly requestDuration: number; - readonly requestMessage: string; + readonly requestMessage: string | TLocalizationToken; /** A string representation of the filters constraints applied to the data request. Also returns as it is in case of whereFilter */ readonly requestFilter: string; readonly source: string | null; @@ -27,7 +41,13 @@ export enum DatabaseDataAccessMode { Readonly, } -export interface IDatabaseDataSource { +export type GetDatabaseDataSourceOptions> = + TSource extends IDatabaseDataSource ? TOptions : never; + +export type GetDatabaseDataSourceResult> = + TSource extends IDatabaseDataSource ? TResult : never; + +export interface IDatabaseDataSource { readonly access: DatabaseDataAccessMode; readonly dataFormat: ResultDataFormat; readonly supportedDataFormats: ResultDataFormat[]; @@ -42,36 +62,34 @@ export interface IDatabaseDataSource; + isError: () => boolean; + isOutdated: () => boolean; isLoadable: () => boolean; isReadonly: (resultIndex: number) => boolean; + hasElementIdentifier: (resultIndex: number) => boolean; + isDataAvailable: (offset: number, count: number) => boolean; isLoading: () => boolean; - isDisabled: (resultIndex: number) => boolean; + isDisabled: (resultIndex?: number) => boolean; hasResult: (resultIndex: number) => boolean; - tryGetAction: (>( - resultIndex: number, - action: IDatabaseDataActionClass, - ) => T | undefined) & - (>(result: TResult, action: IDatabaseDataActionClass) => T | undefined); - getAction: (>(resultIndex: number, action: IDatabaseDataActionClass) => T) & - (>(result: TResult, action: IDatabaseDataActionClass) => T); - getActionImplementation: (>( + tryGetAction: (>(resultIndex: number, action: T) => InstanceType | undefined) & + (>(result: TResult, action: T) => InstanceType | undefined); + getAction: (>(resultIndex: number, action: T) => InstanceType) & + (>(result: TResult, action: T) => InstanceType); + getActionImplementation: (>( resultIndex: number, - action: IDatabaseDataActionInterface, - ) => T | undefined) & - (>( - result: TResult, - action: IDatabaseDataActionInterface, - ) => T | undefined); + action: T, + ) => InstanceType | undefined) & + (>(result: TResult, action: T) => InstanceType | undefined); getResult: (index: number) => TResult | null; + getResults: () => TResult[]; setOutdated: () => this; setResults: (results: TResult[]) => this; @@ -80,17 +98,22 @@ export interface IDatabaseDataSource this; setDataFormat: (dataFormat: ResultDataFormat) => this; setSupportedDataFormats: (dataFormats: ResultDataFormat[]) => this; - setExecutionContext: (context: IConnectionExecutionContext | null) => this; retry: () => Promise; - /** Allows to perform an asynchronous action on the data source, this action will wait previous action to finish and save or load requests. - * The data source will have a loading and disabled state while performing an action */ - runTask: (task: () => Promise) => Promise; - requestData: () => Promise | void; - refreshData: () => Promise | void; - saveData: () => Promise | void; - cancel: () => Promise | void; + /** + * Perform operation with data source. This action should not lead to data lost. Can be cancelled when operation is Task. + * @param operation Task or Promise + * @returns + */ + runOperation: (operation: () => Promise) => Promise; + requestDataPortion(offset: number, count: number): Promise; + requestData: (mutation?: () => void) => Promise; + refreshData: () => Promise; + saveData: () => Promise; + cancel: () => Promise; clearError: () => this; + setError: (error: Error) => this; resetData: () => this; + canSafelyDispose: () => Promise; dispose: () => Promise; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseResultSet.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseResultSet.ts index 355895e5d0..e72d0dcd60 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseResultSet.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseResultSet.ts @@ -1,14 +1,19 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { SqlResultSet } from '@cloudbeaver/core-sdk'; -import type { IDatabaseDataResult } from './IDatabaseDataResult'; +import type { IDatabaseDataResult } from './IDatabaseDataResult.js'; export interface IDatabaseResultSet extends IDatabaseDataResult { + totalCount: number | null; + updateRowCount: number; + projectId: string; + connectionId: string; + contextId: string; data: SqlResultSet | undefined; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Order.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Order.ts index 8c6c86fd0f..b8008202a2 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Order.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Order.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer/src/IDataViewerPageState.ts b/webapp/packages/plugin-data-viewer/src/IDataViewerPageState.ts index 97e00c7709..c1d13ee974 100644 --- a/webapp/packages/plugin-data-viewer/src/IDataViewerPageState.ts +++ b/webapp/packages/plugin-data-viewer/src/IDataViewerPageState.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer/src/IDataViewerTableStorage.ts b/webapp/packages/plugin-data-viewer/src/IDataViewerTableStorage.ts new file mode 100644 index 0000000000..337f960147 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/IDataViewerTableStorage.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel.js'; +import { type IDatabaseDataSource } from './DatabaseDataModel/IDatabaseDataSource.js'; + +export interface IDataViewerTableStorage { + has(tableId: string): boolean; + get = IDatabaseDataModel>(tableId: string): T | undefined; + add = IDatabaseDataSource>(model: IDatabaseDataModel): IDatabaseDataModel; + remove(tableId: string): void; +} diff --git a/webapp/packages/plugin-data-viewer/src/LocaleService.ts b/webapp/packages/plugin-data-viewer/src/LocaleService.ts index e3649a06b4..cd5cdaf6f3 100644 --- a/webapp/packages/plugin-data-viewer/src/LocaleService.ts +++ b/webapp/packages/plugin-data-viewer/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,32 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'de': + return (await import('./locales/de.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-data-viewer/src/MENU_DV_CONTEXT_MENU.ts b/webapp/packages/plugin-data-viewer/src/MENU_DV_CONTEXT_MENU.ts new file mode 100644 index 0000000000..05020ddbc4 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/MENU_DV_CONTEXT_MENU.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_DV_CONTEXT_MENU = createMenu('dv-context-menu', { label: 'Data Editor Context Menu' }); diff --git a/webapp/packages/plugin-data-viewer/src/ResultSet/ACTION_COUNT_TOTAL_ELEMENTS.ts b/webapp/packages/plugin-data-viewer/src/ResultSet/ACTION_COUNT_TOTAL_ELEMENTS.ts new file mode 100644 index 0000000000..063b5809f8 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ResultSet/ACTION_COUNT_TOTAL_ELEMENTS.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_COUNT_TOTAL_ELEMENTS = createAction('data-count-total-elements', { + label: 'ui_count_total_elements', + tooltip: 'data_viewer_total_count_tooltip', + icon: '/icons/data_row_count.svg', +}); diff --git a/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetDataSource.ts b/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetDataSource.ts new file mode 100644 index 0000000000..577c532f8d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetDataSource.ts @@ -0,0 +1,176 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 } from 'mobx'; + +import type { IConnectionExecutionContext, IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; +import type { IServiceProvider } from '@cloudbeaver/core-di'; +import type { ITask } from '@cloudbeaver/core-executor'; +import { AsyncTaskInfoService } from '@cloudbeaver/core-root'; +import type { GraphQLService } from '@cloudbeaver/core-sdk'; + +import { DatabaseDataSource } from '../DatabaseDataModel/DatabaseDataSource.js'; +import { type IDatabaseDataOptions } from '../DatabaseDataModel/IDatabaseDataOptions.js'; +import type { IDatabaseResultSet } from '../DatabaseDataModel/IDatabaseResultSet.js'; + +export abstract class ResultSetDataSource extends DatabaseDataSource { + executionContext: IConnectionExecutionContext | null; + totalCountRequestTask: ITask | null; + private keepExecutionContextOnDispose: boolean; + + constructor( + override readonly serviceProvider: IServiceProvider, + protected graphQLService: GraphQLService, + protected asyncTaskInfoService: AsyncTaskInfoService, + ) { + super(serviceProvider); + this.totalCountRequestTask = null; + this.executionContext = null; + this.keepExecutionContextOnDispose = false; + + makeObservable(this, { + totalCountRequestTask: observable.ref, + executionContext: observable, + }); + } + + override isReadonly(resultIndex: number): boolean { + return super.isReadonly(resultIndex) || !this.executionContext?.context || !!this.getResult(resultIndex)?.data?.readOnly; + } + + override async cancel(): Promise { + await super.cancel(); + await this.cancelLoadTotalCount(); + } + + async cancelLoadTotalCount(): Promise | null> { + await this.totalCountRequestTask?.cancel(); + + return this.totalCountRequestTask; + } + + async loadTotalCount(resultIndex: number): Promise> { + const executionContext = this.executionContext; + const executionContextInfo = this.executionContext?.context; + + if (!executionContext || !executionContextInfo) { + throw new Error('Context must be provided'); + } + + const result = this.getResult(resultIndex); + + if (!result?.id) { + throw new Error('Result id must be provided'); + } + + const asyncTask = this.asyncTaskInfoService.create(async () => { + const { taskInfo } = await this.graphQLService.sdk.asyncSqlRowDataCount({ + resultsId: result.id!, + connectionId: executionContextInfo.connectionId, + contextId: executionContextInfo.id, + projectId: executionContextInfo.projectId, + }); + + return taskInfo; + }); + + const task = executionContext.run( + async () => { + const info = await this.asyncTaskInfoService.run(asyncTask); + + const { count } = await this.graphQLService.sdk.getSqlRowDataCountResult({ taskId: info.id }); + + return count; + }, + () => this.asyncTaskInfoService.cancel(asyncTask.id), + () => this.asyncTaskInfoService.remove(asyncTask.id), + ); + + this.totalCountRequestTask = task; + + const count = await task; + this.setTotalCount(resultIndex, count); + + return this.totalCountRequestTask; + } + + override setResults(results: IDatabaseResultSet[]): this { + this.closeResults(this.results.filter(result => !results.some(r => r.id === result.id))); + return super.setResults(results); + } + + override async dispose(): Promise { + await super.dispose(); + if (this.keepExecutionContextOnDispose) { + await this.closeResults(this.results); + } else { + await this.executionContext?.destroy(); + } + } + + setKeepExecutionContextOnDispose(keep: boolean): this { + this.keepExecutionContextOnDispose = keep; + return this; + } + + setExecutionContext(context: IConnectionExecutionContext | null): this { + this.executionContext = context; + this.setOutdated(); + return this; + } + + protected getPreviousResultId(prevResults: IDatabaseResultSet[], context: IConnectionExecutionContextInfo) { + let resultId: string | undefined; + + if ( + prevResults.length === 1 && + prevResults[0]!.contextId === context.id && + prevResults[0]!.connectionId === context.connectionId && + prevResults[0]!.id !== null + ) { + resultId = prevResults[0]!.id; + } + + return resultId; + } + + private setTotalCount(resultIndex: number, count: number): this { + const result = this.getResult(resultIndex); + + if (result) { + result.totalCount = count; + } + return this; + } + + private async closeResults(results: IDatabaseResultSet[]): Promise { + if (!this.executionContext?.context) { + return; + } + + for (const result of results) { + // TODO: it's better to track that context is closed with subscription + if (result.id === null || result.contextId !== this.executionContext.context.id) { + continue; + } + try { + await this.graphQLService.sdk.closeResult({ + projectId: result.projectId, + connectionId: result.connectionId, + contextId: result.contextId, + resultId: result.id, + }); + } catch (exception: any) { + console.log(`Error closing result (${result.id}):`, exception); + } + } + } +} + +export function isResultSetDataSource(dataSource: any): dataSource is ResultSetDataSource { + return dataSource instanceof ResultSetDataSource; +} diff --git a/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetTableFooterMenuService.ts b/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetTableFooterMenuService.ts new file mode 100644 index 0000000000..a26f3a73c3 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetTableFooterMenuService.ts @@ -0,0 +1,189 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; +import { injectable } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ActionService, MenuService } from '@cloudbeaver/core-view'; + +import { DatabaseDataConstraintAction } from '../DatabaseDataModel/Actions/DatabaseDataConstraintAction.js'; +import { DatabaseMetadataAction } from '../DatabaseDataModel/Actions/DatabaseMetadataAction.js'; +import { DATA_CONTEXT_DV_DDM } from '../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.js'; +import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.js'; +import { type IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel.js'; +import { type IDatabaseDataOptions } from '../DatabaseDataModel/IDatabaseDataOptions.js'; +import { DATA_VIEWER_DATA_MODEL_ACTIONS_MENU } from '../TableViewer/TableFooter/TableFooterMenu/DATA_VIEWER_DATA_MODEL_ACTIONS_MENU.js'; +import { ACTION_COUNT_TOTAL_ELEMENTS } from './ACTION_COUNT_TOTAL_ELEMENTS.js'; +import { isResultSetDataModel } from './isResultSetDataModel.js'; +import { ResultSetDataSource } from './ResultSetDataSource.js'; + +interface IResultSetActionsMetadata { + totalCount: { + loading: boolean; + }; +} + +@injectable(() => [ActionService, MenuService, NotificationService]) +export class ResultSetTableFooterMenuService { + constructor( + private readonly actionService: ActionService, + private readonly menuService: MenuService, + private readonly notificationService: NotificationService, + ) {} + + register() { + this.menuService.addCreator({ + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isApplicable(context) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const result = model.source.getResult(resultIndex); + + return !!result; + }, + getItems(context, items) { + return [ACTION_COUNT_TOTAL_ELEMENTS, ...items]; + }, + }); + this.actionService.addHandler({ + id: 'result-set-data-base-handler', + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + actions: [ACTION_COUNT_TOTAL_ELEMENTS], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isActionApplicable(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)! as any; + + if (!isResultSetDataModel(model)) { + return false; + } + + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const result = model.source.getResult(resultIndex); + + if (!result) { + return false; + } + const constraint = model.source.tryGetAction(resultIndex, DatabaseDataConstraintAction); + + switch (action) { + case ACTION_COUNT_TOTAL_ELEMENTS: { + return !!constraint?.supported && !model.isDisabled(resultIndex); + } + } + return true; + }, + isDisabled: (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)! as unknown as IDatabaseDataModel; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + if (model.isLoading() || model.isDisabled(resultIndex) || !model.source.getResult(resultIndex)) { + return true; + } + + switch (action) { + case ACTION_COUNT_TOTAL_ELEMENTS: { + const metadata = this.getState(context); + + return metadata.totalCount.loading && Boolean(model.source.totalCountRequestTask?.cancelled); + } + } + + return false; + }, + isLoading: (context, action) => { + const metadata = this.getState(context); + + switch (action) { + case ACTION_COUNT_TOTAL_ELEMENTS: { + return metadata.totalCount.loading; + } + } + + return false; + }, + getActionInfo: (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)! as unknown as IDatabaseDataModel; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const metadata = this.getState(context); + + switch (action) { + case ACTION_COUNT_TOTAL_ELEMENTS: { + const result = model.source.getResult(resultIndex); + if (!result) { + return action.info; + } + + let label = action.info.label; + let icon = action.info.icon; + + if (metadata.totalCount.loading) { + const cancelling = Boolean(model.source.totalCountRequestTask?.cancelled); + label = cancelling ? 'ui_processing_canceling' : 'ui_processing_cancel'; + icon = 'cross'; + } else { + const currentCount = result.loadedFully ? result.count : `${result.count}+`; + label = String(result.totalCount ?? currentCount); + } + + return { ...action.info, label, icon }; + } + } + + return action.info; + }, + handler: async (context, action) => { + const model = context.get(DATA_CONTEXT_DV_DDM)! as unknown as IDatabaseDataModel; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const metadata = this.getState(context); + + switch (action) { + case ACTION_COUNT_TOTAL_ELEMENTS: { + if (metadata.totalCount.loading) { + if (model.source.totalCountRequestTask?.cancelled) { + // Cancel request + return; + } + + try { + await model.source.cancelLoadTotalCount(); + } catch (e: any) { + if (!model.source.totalCountRequestTask?.cancelled) { + this.notificationService.logException(e); + } + } + } else { + try { + metadata.totalCount.loading = true; + await model.source.loadTotalCount(resultIndex); + } catch (exception: any) { + if (model.source.totalCountRequestTask?.cancelled) { + this.notificationService.logInfo({ + title: 'data_viewer_total_count_canceled_title', + message: 'data_viewer_total_count_canceled_message', + }); + } else { + this.notificationService.logException(exception, 'data_viewer_total_count_failed'); + } + } finally { + metadata.totalCount.loading = false; + } + } + break; + } + } + }, + }); + } + + private getState(context: IDataContextProvider) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const metadataAction = model.source.getAction(resultIndex, DatabaseMetadataAction); + return metadataAction.get('result-set-database-metadata', () => ({ totalCount: { loading: false } })); + } +} diff --git a/webapp/packages/plugin-data-viewer/src/ResultSet/isResultSetDataModel.ts b/webapp/packages/plugin-data-viewer/src/ResultSet/isResultSetDataModel.ts new file mode 100644 index 0000000000..ff75d44860 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ResultSet/isResultSetDataModel.ts @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { DatabaseDataModel } from '../DatabaseDataModel/DatabaseDataModel.js'; +import { type IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel.js'; +import { type IDatabaseDataOptions } from '../DatabaseDataModel/IDatabaseDataOptions.js'; +import { ResultSetDataSource } from './ResultSetDataSource.js'; + +export function isResultSetDataModel( + dataModel: IDatabaseDataModel | undefined | null, +): dataModel is IDatabaseDataModel> { + return dataModel instanceof DatabaseDataModel && dataModel.source instanceof ResultSetDataSource; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/DATA_CONTEXT_DV_ACTIONS.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/DATA_CONTEXT_DV_ACTIONS.ts new file mode 100644 index 0000000000..8c7faf42af --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/DATA_CONTEXT_DV_ACTIONS.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createDataContext } from '@cloudbeaver/core-data-context'; + +import type { IDataTableActions } from './IDataTableActions.js'; + +export const DATA_CONTEXT_DV_ACTIONS = createDataContext('data-viewer-database-actions'); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/DATA_CONTEXT_DV_PRESENTATION_ACTIONS.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/DATA_CONTEXT_DV_PRESENTATION_ACTIONS.ts new file mode 100644 index 0000000000..1964738e53 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/DATA_CONTEXT_DV_PRESENTATION_ACTIONS.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createDataContext } from '@cloudbeaver/core-data-context'; + +import type { IResultSetElementKey } from '../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.js'; +import type { IDataPresentationActions } from './IDataPresentationActions.js'; + +export const DATA_CONTEXT_DV_PRESENTATION_ACTIONS = createDataContext>( + 'data-viewer-database-presentation-actions', +); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/DATA_CONTEXT_DV_SIMPLE.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/DATA_CONTEXT_DV_SIMPLE.ts new file mode 100644 index 0000000000..82830f90a0 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/DATA_CONTEXT_DV_SIMPLE.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createDataContext } from '@cloudbeaver/core-data-context'; + +export const DATA_CONTEXT_DV_SIMPLE = createDataContext('data-viewer-database-simple'); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/DataPresentation.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/DataPresentation.module.css new file mode 100644 index 0000000000..79cb87d56d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/DataPresentation.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.presentation { + flex: 1; + overflow: auto; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/DataPresentation.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/DataPresentation.tsx new file mode 100644 index 0000000000..b8a3e15432 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/DataPresentation.tsx @@ -0,0 +1,66 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import type { HTMLAttributes } from 'react'; + +import { s, TextPlaceholder } from '@cloudbeaver/core-blocks'; +import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; + +import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel.js'; +import type { IDataPresentationOptions } from '../DataPresentationService.js'; +import styles from './DataPresentation.module.css'; +import type { IDataTableActions } from './IDataTableActions.js'; +import { TableStatistics } from './TableStatistics.js'; + +interface Props extends HTMLAttributes { + model: IDatabaseDataModel; + actions: IDataTableActions; + dataFormat: ResultDataFormat; + presentation: IDataPresentationOptions; + resultIndex: number; + simple: boolean; + isStatistics: boolean; +} + +export const DataPresentation = observer(function DataPresentation({ + model, + actions, + dataFormat, + presentation, + resultIndex, + simple, + isStatistics, + ...rest +}) { + if ((presentation.dataFormat !== undefined && dataFormat !== presentation.dataFormat) || !model.source.hasResult(resultIndex)) { + if (model.isLoading()) { + return null; + } + + // eslint-disable-next-line react/no-unescaped-entities + return Current data can't be displayed by selected presentation; + } + + const Presentation = presentation.getPresentationComponent(); + + if (isStatistics) { + return ; + } + + return ( + + ); +}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/DataViewerViewService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/DataViewerViewService.ts new file mode 100644 index 0000000000..b03d2e7357 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/DataViewerViewService.ts @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { ACTION_REDO, ACTION_SAVE, ACTION_UNDO, type IActiveView, View } from '@cloudbeaver/core-view'; +import { type ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; + +@injectable(() => [NavigationTabsService]) +export class DataViewerViewService extends View { + constructor(private readonly navigationTabsService: NavigationTabsService) { + super(); + this.registerAction(ACTION_UNDO, ACTION_REDO, ACTION_SAVE); + } + + getView(): IActiveView | null { + return this.navigationTabsService.getView(); + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/IDataPresentationActions.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/IDataPresentationActions.ts index babae22270..28ff12175a 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/IDataPresentationActions.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/IDataPresentationActions.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/IDataTableActions.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/IDataTableActions.ts index eee9050a1c..36bbbfbea8 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/IDataTableActions.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/IDataTableActions.ts @@ -1,18 +1,18 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; +import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel.js'; export interface IDataTableActions { presentationId: string | undefined; valuePresentationId: string | null | undefined; - dataModel: IDatabaseDataModel | undefined; + dataModel: IDatabaseDataModel | undefined; setPresentation: (id: string) => void; setValuePresentation: (id: string | null) => void; diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/SqlEditorSessionClosedDialog.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/SqlEditorSessionClosedDialog.module.css new file mode 100644 index 0000000000..ecf9e9afb4 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/SqlEditorSessionClosedDialog.module.css @@ -0,0 +1,42 @@ +.container { + display: flex; + flex-direction: column; + gap: 16px; +} + +.message { + color: var(--theme-text-on-surface); + line-height: 1.5; +} + +.queryInfo { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px; + background-color: var(--theme-background-secondary); + border-radius: 4px; + border: 1px solid var(--theme-divider); +} + +.queryLabel { + font-weight: 500; + color: var(--theme-text-on-surface); + font-size: 14px; +} + +.queryPreview { + font-family: monospace; + font-size: 13px; + color: var(--theme-text-on-surface-secondary); + word-break: break-all; + white-space: pre-wrap; + max-height: 120px; + overflow-y: auto; +} + +.footer { + display: flex; + align-items: center; + gap: 12px; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/SqlEditorSessionClosedDialog.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/SqlEditorSessionClosedDialog.tsx new file mode 100644 index 0000000000..4ae795df48 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/SqlEditorSessionClosedDialog.tsx @@ -0,0 +1,65 @@ +import { + Button, + Fill, + Translate, + CommonDialogBody, + CommonDialogHeader, + CommonDialogWrapper, + s, + CommonDialogFooter, + useS, +} from '@cloudbeaver/core-blocks'; +import style from './SqlEditorSessionClosedDialog.module.css'; + +import type { DialogComponent } from '@cloudbeaver/core-dialogs'; + +export interface SqlEditorSessionClosedDialogPayload { + query?: string; +} + +export const SqlEditorSessionClosedDialog: DialogComponent = function SqlSessionClosedDialog({ + payload, + resolveDialog, + rejectDialog, + className, +}) { + const styles = useS(style); + + return ( + + + +
+
+ +
+ {payload.query && ( +
+
+ +
+
+ {payload.query.length > 100 ? `${payload.query.substring(0, 100)}...` : payload.query} +
+
+ )} +
+
+ + + + + +
+ ); +}; diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableError.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableError.module.css new file mode 100644 index 0000000000..68174b2836 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableError.module.css @@ -0,0 +1,79 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.error { + composes: theme-background-surface theme-text-on-surface from global; + position: absolute; + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 16px; + overflow: auto; + pointer-events: none; + bottom: 0; + right: 0; + z-index: 1; + opacity: 0; + transition: + opacity 0.3s ease-in-out, + width 0.3s ease-in-out, + height 0.3s ease-in-out, + background 0.3s ease-in-out; +} +.errorBody { + display: flex; + gap: 24px; + margin-bottom: 24px; +} +.errorMessage { + white-space: pre-wrap; +} +.errorSubMessage { + display: block; + font-size: 0.9em; + margin-top: 8px; +} +.iconOrImage { + width: 40px; + height: 40px; +} +.controls { + display: flex; + gap: 16px; + & > .button { + flex-shrink: 0; + } +} + +.collapsed { + pointer-events: auto; + width: 92px; + height: 72px; + background: transparent !important; + + & .iconOrImage { + cursor: pointer; + } + &.animated { + overflow: hidden; + } + & .errorMessage, + & .controls { + display: none; + } +} + +.animated { + overflow: auto; + pointer-events: auto; + opacity: 1; +} +.errorHidden { + pointer-events: none; + overflow: hidden; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableError.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableError.tsx index d5afa9c4d9..b756246a62 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableError.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableError.tsx @@ -1,82 +1,30 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import styled, { css, use } from 'reshadow'; +import { useCallback, useEffect } from 'react'; +import { compressToEncodedURIComponent } from 'lz-string'; -import { Button, IconOrImage, useErrorDetails, useObservableRef, useStateDelay, useTranslate } from '@cloudbeaver/core-blocks'; +import { Button, IconOrImage, Placeholder, s, useErrorDetails, useObservableRef, useS, useStateDelay, useTranslate } from '@cloudbeaver/core-blocks'; import { ServerErrorType, ServerInternalError } from '@cloudbeaver/core-sdk'; import { errorOf } from '@cloudbeaver/core-utils'; - -import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; - -const style = css` - error { - composes: theme-background-surface theme-text-on-surface from global; - position: absolute; - box-sizing: border-box; - width: 100%; - height: 100%; - padding: 16px; - overflow: auto; - pointer-events: none; - bottom: 0; - right: 0; - z-index: 1; - opacity: 0; - transition: opacity 0.3s ease-in-out, width 0.3s ease-in-out, height 0.3s ease-in-out, background 0.3s ease-in-out; - - &[|animated] { - overflow: hidden; - pointer-events: auto; - opacity: 1; - } - &[|collapsed] { - pointer-events: auto; - width: 92px; - height: 72px; - background: transparent !important; - - & IconOrImage { - cursor: pointer; - } - - & error-message, - & controls { - display: none; - } - } - &[|errorHidden] { - pointer-events: none; - overflow: hidden; - } - } - error-body { - display: flex; - gap: 24px; - align-items: center; - margin-bottom: 24px; - } - error-message { - white-space: pre-wrap; - } - IconOrImage { - width: 40px; - height: 40px; - } - controls { - display: flex; - gap: 16px; - & > Button { - flex-shrink: 0; - } - } -`; +import { useService } from '@cloudbeaver/core-di'; + +import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel.js'; +import { DataViewerService } from '../DataViewerService.js'; +import styles from './TableError.module.css'; +import { ConnectionSchemaManagerService } from '@cloudbeaver/plugin-datasource-context-switch'; +import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; +import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { LocalStorageSqlDataSource, QueryDataSource, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor'; +import { isSQLEditorTab, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab'; +import { SqlEditorSessionClosedDialog } from './SqlEditorSessionClosedDialog.js'; interface Props { model: IDatabaseDataModel; @@ -91,8 +39,21 @@ interface ErrorInfo { show: () => void; } +const WORKFLOW_EXEC_SUCCESS_ERROR_CODE = 'workflow_success'; + export const TableError = observer(function TableError({ model, loading, className }) { const translate = useTranslate(); + + const connectionSchemaManagerService = useService(ConnectionSchemaManagerService); + const sqlDataSourceService = useService(SqlDataSourceService); + const navigationTabsService = useService(NavigationTabsService); + const commonDialogService = useService(CommonDialogService); + const sqlEditorNavigatorService = useService(SqlEditorNavigatorService); + const connectionInfo = useService(ConnectionInfoResource); + + const style = useS(styles); + const dataViewerService = useService(DataViewerService); + const errorInfo = useObservableRef( () => ({ error: null, @@ -109,12 +70,6 @@ export const TableError = observer(function TableError({ model, loading, }, false, ); - - if (errorInfo.error !== model.source.error) { - errorInfo.error = model.source.error || null; - errorInfo.display = !!model.source.error; - } - const internalServerError = errorOf(model.source.error, ServerInternalError); const error = useErrorDetails(model.source.error); const animated = useStateDelay(!!errorInfo.error && !loading, 1); @@ -122,12 +77,40 @@ export const TableError = observer(function TableError({ model, loading, const errorHidden = errorInfo.error === null; const quote = internalServerError?.errorType === ServerErrorType.QUOTE_EXCEEDED; + const onCreateWorkflowNavigate = () => { + const [projectName, instanceName] = connectionSchemaManagerService.currentConnection?.name.split(':') ?? []; + const schema = connectionSchemaManagerService.currentObjectCatalog?.name; + const sql = sqlDataSourceService.get(navigationTabsService.getView()?.context.id ?? '')?.script; + + const data = { + instanceName, + schema, + sql, + }; + + window.open( + `/transit?from=cloudbeaver&to=create_workflow&project_name=${projectName}&compression_data=${compressToEncodedURIComponent( + JSON.stringify(data), + )}`, + ); + }; + + const onWorkflowDetailNavigate = (workflowId: string) => { + const [projectName] = connectionSchemaManagerService.currentConnection?.name.split(':') ?? []; + + window.open(`/transit?from=cloudbeaver&to=workflow_detail&workflow_id=${workflowId}&project_name=${projectName}`); + }; + let icon = '/icons/error_icon.svg'; if (quote) { icon = '/icons/info_icon.svg'; } + if (error.errorCode === WORKFLOW_EXEC_SUCCESS_ERROR_CODE) { + icon = '/icons/success_icon.svg'; + } + let onRetry = () => model.retry(); if (error.refresh) { @@ -139,25 +122,110 @@ export const TableError = observer(function TableError({ model, loading, }; } - return styled(style)( - - - errorInfo.show()} /> - {error.message} - - - {error.hasDetails && ( - )} - - - , + {error.workflowId ? ( + + ) : ( + + )} + +
+
); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshButton.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshButton.tsx deleted file mode 100644 index f1c9999b7e..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshButton.tsx +++ /dev/null @@ -1,140 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { Icon, Menu, MenuItem, MenuItemElement, TimerIcon, useTranslate } from '@cloudbeaver/core-blocks'; -import { declensionOfNumber } from '@cloudbeaver/core-utils'; - -import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel'; -import { useAutoRefresh } from './useAutoRefresh'; - -const styles = css` - auto-reload { - composes: theme-text-primary theme-ripple from global; - height: 100%; - display: flex; - cursor: pointer; - align-items: center; - - & icon-box { - position: relative; - padding-left: 8px; - display: flex; - align-items: center; - - & Icon { - width: 16px; - height: 16px; - flex-grow: 0; - flex-shrink: 0; - } - - &:hover > Icon :global(use) { - fill: var(--theme-primary) !important; - } - } - - & arrow-box { - position: relative; - height: 100%; - display: flex; - align-items: center; - padding-right: 8px; - - & > Icon { - width: 14px; - height: 14px; - flex-grow: 0; - flex-shrink: 0; - } - - &:hover > Icon :global(use) { - fill: var(--theme-primary) !important; - } - } - } -`; - -interface Props { - model: IDatabaseDataModel; - disabled?: boolean; -} - -const intervals = [5, 10, 15, 30, 60]; - -export const AutoRefreshButton = observer(function AutoRefreshButton({ model, disabled }) { - const translate = useTranslate(); - const autoRefresh = useAutoRefresh(model); - const interval = autoRefresh.settings.interval; - const intervals_messages: string[] = []; - - const buttonTitle = translate(interval === null ? 'data_viewer_action_refresh' : 'data_viewer_action_auto_refresh_stop'); - - function handleClick() { - if (disabled) { - return; - } - - if (interval === null) { - model.refresh(); - } else { - autoRefresh.stop(); - } - } - - for (let interval of intervals) { - let message = ['ui_second_first_form', 'ui_second_second_form', 'ui_second_third_form']; - - if (interval >= 60) { - message = ['ui_minute_first_form', 'ui_minute_second_form', 'ui_minute_third_form']; - interval = Math.round(interval / 60); - } - - intervals_messages.push(translate(declensionOfNumber(interval, message), undefined, { interval })); - } - - return styled(styles)( - - - {interval === null ? : } - - - - - - {intervals.map((inte, i) => ( - autoRefresh.setInterval(inte)} - > - - - ))} - - - - - } - disabled={disabled} - modal - disclosure - > - - - - - , - ); -}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshSettingsDialog.m.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshSettingsDialog.m.css deleted file mode 100644 index 60a7264457..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshSettingsDialog.m.css +++ /dev/null @@ -1,19 +0,0 @@ -.footerContainer { - display: flex; - width: min-content; - flex: 1; - align-items: center; - justify-content: flex-end; - gap: 24px; -} -.buttons { - flex: 1; - display: flex; - gap: 24px; -} -.wrapper { - display: flex; - height: 100%; - width: 100%; - overflow: auto; -} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshSettingsDialog.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshSettingsDialog.tsx deleted file mode 100644 index 32737807f1..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshSettingsDialog.tsx +++ /dev/null @@ -1,90 +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 { observer } from 'mobx-react-lite'; -import { useRef } from 'react'; - -import { - Button, - CommonDialogBody, - CommonDialogFooter, - CommonDialogHeader, - CommonDialogWrapper, - Container, - FieldCheckbox, - Fill, - Form, - Group, - InputField, - s, - useS, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import type { DialogComponentProps } from '@cloudbeaver/core-dialogs'; - -import style from './AutoRefreshSettingsDialog.m.css'; -import type { IAutoRefreshSettings } from './IAutoRefreshSettings'; - -interface Payload { - settings: IAutoRefreshSettings; -} - -export const AutoRefreshSettingsDialog = observer>(function AutoRefreshSettingsDialog({ - rejectDialog, - resolveDialog, - payload, -}) { - const translate = useTranslate(); - const styles = useS(style); - const formRef = useRef(null); - - function resolve() { - formRef.current?.focus(); - const valid = formRef.current?.checkValidity(); - formRef.current?.reportValidity(); - - if (valid) { - resolveDialog(); - } - } - - return ( - - - -
-
resolve()}> - - - - {translate('ui_interval')} - - - - {translate('data_viewer_auto_refresh_settings_stop_on_error')} - - - -
-
-
- -
-
- - - -
-
-
-
- ); -}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/IAutoRefreshSettings.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/IAutoRefreshSettings.ts deleted file mode 100644 index 2c7e17793d..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/IAutoRefreshSettings.ts +++ /dev/null @@ -1,12 +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. - */ - -export interface IAutoRefreshSettings { - interval: number | null; - stopOnError: boolean; -} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/useAutoRefresh.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/useAutoRefresh.ts deleted file mode 100644 index b382e4ce1e..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/useAutoRefresh.ts +++ /dev/null @@ -1,83 +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 { observable } from 'mobx'; - -import { useInterval, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; - -import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel'; -import { AutoRefreshSettingsDialog } from './AutoRefreshSettingsDialog'; -import type { IAutoRefreshSettings } from './IAutoRefreshSettings'; - -interface IAutoRefresh { - settings: IAutoRefreshSettings; - configure: () => Promise; - setInterval: (interval: number) => void; - stop: () => void; -} - -export function useAutoRefresh(model: IDatabaseDataModel) { - const commonDialogService = useService(CommonDialogService); - const settings = useObservableRef( - () => ({ - interval: null, - stopOnError: true, - }), - { - interval: observable, - stopOnError: observable, - }, - false, - ); - - useInterval( - async () => { - try { - await model.refresh(); - } catch {} - - if (model.source.error && settings.stopOnError) { - settings.interval = null; - } - }, - settings.interval !== null ? settings.interval * 1000 : null, - ); - - return useObjectRef( - () => ({ - async configure() { - const settings: IAutoRefreshSettings = observable({ ...this.settings }); - - const result = await commonDialogService.open(AutoRefreshSettingsDialog, { settings }); - - if (result === DialogueStateResult.Resolved) { - let interval = settings.interval; - - if (typeof interval === 'string') { - interval = Number.parseInt(interval); - } - - if (!Number.isInteger(interval)) { - interval = null; - } - - Object.assign(this.settings, { ...settings, interval }); - } - }, - setInterval(interval: number) { - this.settings.interval = interval; - }, - stop() { - this.settings.interval = null; - }, - }), - { settings }, - ['configure', 'setInterval', 'stop'], - ); -} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.module.css new file mode 100644 index 0000000000..8ba6e0099b --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.module.css @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.time { + composes: theme-typography--caption from global; + white-space: nowrap; + padding: 0 4px; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx index db21fd2988..d9dd475b21 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx @@ -1,133 +1,41 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import styled, { css, use } from 'reshadow'; +import type { HTMLAttributes } from 'react'; -import { Form, getComputed, ToolsPanel } from '@cloudbeaver/core-blocks'; -import type { IDataContext } from '@cloudbeaver/core-data-context'; -import { useService } from '@cloudbeaver/core-di'; +import { Container, Fill, s, ToolsPanel, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; -import { DataViewerSettingsService } from '../../DataViewerSettingsService'; -import { AutoRefreshButton } from './AutoRefresh/AutoRefreshButton'; -import { TableFooterMenu } from './TableFooterMenu/TableFooterMenu'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import styles from './TableFooter.module.css'; +import { TableFooterMenu } from './TableFooterMenu/TableFooterMenu.js'; -const tableFooterStyles = css` - ToolsPanel { - align-items: center; - flex: 0 0 auto; - overflow: auto; - min-height: 32px; - height: initial; - } - count input, - count placeholder { - height: 26px; - width: 80px; - box-sizing: border-box; - padding: 4px 7px; - font-size: 13px; - line-height: 24px; - } - reload { - composes: theme-text-primary theme-ripple from global; - height: 100%; - display: flex; - cursor: pointer; - align-items: center; - padding: 0 16px; - - & IconOrImage { - & :global(use) { - fill: var(--theme-primary) !important; - } - width: 24px; - height: 24px; - } - } - IconButton { - position: relative; - height: 24px; - width: 24px; - display: block; - } - time { - composes: theme-typography--caption from global; - white-space: nowrap; - margin-left: auto; - margin-right: 16px; - } - count { - padding: 0 4px; - } -`; - -interface Props { +interface Props extends HTMLAttributes { resultIndex: number; - model: IDatabaseDataModel; + model: IDatabaseDataModel; simple: boolean; - context?: IDataContext; } -export const TableFooter = observer(function TableFooter({ resultIndex, model, simple, context }) { - const ref = useRef(null); - const [limit, setLimit] = useState(model.countGain + ''); - const dataViewerSettingsService = useService(DataViewerSettingsService); - - const handleChange = useCallback(async () => { - if (!ref.current) { - return; - } - - const value = dataViewerSettingsService.getDefaultRowsCount(parseInt(ref.current.value, 10)); - - setLimit(value + ''); - if (model.countGain !== value) { - await model.setCountGain(value).reload(); - } - }, [model]); - - useEffect(() => { - if (limit !== model.countGain + '') { - setLimit(model.countGain + ''); - } - }, [model.countGain]); - - const disabled = getComputed(() => model.isLoading() || model.isDisabled(resultIndex)); - - return styled(tableFooterStyles)( - - {/* model.refresh()}> - - */} - - -
- setLimit(e.target.value)} - onBlur={handleChange} - {...use({ mod: 'surface' })} - /> -
-
- - {model.source.requestInfo.requestMessage.length > 0 && ( - +export const TableFooter = observer(function TableFooter({ resultIndex, model, simple, ...rest }) { + const translate = useTranslate(); + const style = useS(styles); + + return ( + + + {model.source.requestInfo.requestMessage && ( + <> + + + {translate(model.source.requestInfo.requestMessage)} - {model.source.requestInfo.requestDuration} + {translate('ui_ms')} + + )} - , +
); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/DATA_VIEWER_DATA_MODEL_ACTIONS_MENU.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/DATA_VIEWER_DATA_MODEL_ACTIONS_MENU.ts index 7847d63c15..d112833104 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/DATA_VIEWER_DATA_MODEL_ACTIONS_MENU.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/DATA_VIEWER_DATA_MODEL_ACTIONS_MENU.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const DATA_VIEWER_DATA_MODEL_ACTIONS_MENU = createMenu('data-viewer-data-model-actions', 'Data viewer data model actions menu'); +export const DATA_VIEWER_DATA_MODEL_ACTIONS_MENU = createMenu('data-viewer-data-model-actions', { label: 'Data Editor data model actions menu' }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/FetchSizeAction.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/FetchSizeAction.module.css new file mode 100644 index 0000000000..52b16f197c --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/FetchSizeAction.module.css @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.count { + padding: 0 2px; + + & .input { + height: 26px; + width: 80px; + box-sizing: border-box; + padding: 4px 7px; + font-size: 13px; + line-height: 24px; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/FetchSizeAction.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/FetchSizeAction.tsx new file mode 100644 index 0000000000..dcd50c12a7 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/FetchSizeAction.tsx @@ -0,0 +1,64 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import { useEffect, useRef, useState } from 'react'; + +import { Container, Form, getComputed, s, useS } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { type ICustomMenuItemComponent } from '@cloudbeaver/core-view'; + +import { DATA_CONTEXT_DV_DDM } from '../../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.js'; +import { DataViewerSettingsService } from '../../../../DataViewerSettingsService.js'; +import styles from './FetchSizeAction.module.css'; + +export const FetchSizeAction: ICustomMenuItemComponent = observer(function FetchSizeAction({ context }) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const ref = useRef(null); + const [limit, setLimit] = useState(model.countGain + ''); + const dataViewerSettingsService = useService(DataViewerSettingsService); + const style = useS(styles); + + async function handleChange() { + if (!ref.current) { + return; + } + + const value = dataViewerSettingsService.getDefaultRowsCount(parseInt(ref.current.value, 10)); + + setLimit(value + ''); + if (model.countGain !== value) { + await model.setCountGain(value).reload(); + } + } + + useEffect(() => { + if (limit !== model.countGain + '') { + setLimit(model.countGain + ''); + } + }, [model.countGain]); + + const disabled = getComputed(() => model.isLoading()); + + return ( + +
+ setLimit(e.target.value)} + onBlur={handleChange} + /> +
+
+ ); +}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/TableFetchSizeActionBootstrap.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/TableFetchSizeActionBootstrap.ts new file mode 100644 index 0000000000..bf477c8143 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/TableFetchSizeActionBootstrap.ts @@ -0,0 +1,53 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { MenuCustomItem, MenuService } from '@cloudbeaver/core-view'; + +import { DATA_CONTEXT_DV_DDM } from '../../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.js'; +import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.js'; +import { DATA_VIEWER_DATA_MODEL_ACTIONS_MENU } from '../DATA_VIEWER_DATA_MODEL_ACTIONS_MENU.js'; + +const FetchSizeAction = importLazyComponent(() => import('./FetchSizeAction.js').then(module => module.FetchSizeAction)); + +@injectable(() => [MenuService]) +export class TableFetchSizeActionBootstrap extends Bootstrap { + constructor(private readonly menuService: MenuService) { + super(); + } + + override register() { + this.registerGeneralActions(); + } + + private registerGeneralActions() { + this.menuService.addCreator({ + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + contexts: [DATA_CONTEXT_DV_DDM], + isApplicable(context) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + + return resultIndex !== undefined && !model.isDisabled(resultIndex); + }, + getItems(context, items) { + return [ + new MenuCustomItem({ + id: 'fetch-size', + getComponent: () => FetchSizeAction, + }), + ...items, + ]; + }, + // orderItems(context, items) { + // const extracted = menuExtractItems(items, [MENU_DATA_VIEWER_AUTO_REFRESH]); + // return [...extracted, ...items]; + // }, + }); + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/AutoRefreshSettingsDialog.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/AutoRefreshSettingsDialog.module.css new file mode 100644 index 0000000000..ce87160fee --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/AutoRefreshSettingsDialog.module.css @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.footerContainer { + display: flex; + width: min-content; + flex: 1; + align-items: center; + justify-content: flex-end; + gap: 24px; +} +.buttons { + flex: 1; + display: flex; + gap: 24px; +} +.wrapper { + display: flex; + height: 100%; + width: 100%; + overflow: auto; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/AutoRefreshSettingsDialog.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/AutoRefreshSettingsDialog.tsx new file mode 100644 index 0000000000..96894f0a76 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/AutoRefreshSettingsDialog.tsx @@ -0,0 +1,88 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { useRef } from 'react'; + +import { + Button, + CommonDialogBody, + CommonDialogFooter, + CommonDialogHeader, + CommonDialogWrapper, + Container, + FieldCheckbox, + Fill, + Form, + Group, + InputField, + s, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import type { DialogComponentProps } from '@cloudbeaver/core-dialogs'; + +import type { IDatabaseRefreshState } from '../../../../DatabaseDataModel/Actions/DatabaseRefreshAction.js'; +import style from './AutoRefreshSettingsDialog.module.css'; + +interface Payload { + state: IDatabaseRefreshState; +} + +export const AutoRefreshSettingsDialog = observer>(function AutoRefreshSettingsDialog({ + rejectDialog, + resolveDialog, + payload: { state }, +}) { + const translate = useTranslate(); + const styles = useS(style); + const formRef = useRef(null); + + function resolve() { + formRef.current?.focus(); + const valid = formRef.current?.checkValidity(); + formRef.current?.reportValidity(); + + if (valid) { + resolveDialog(); + } + } + + return ( + + + +
+
resolve()}> + + + + {translate('ui_interval')} + + + + {translate('plugin_data_viewer_auto_refresh_settings_stop_on_error')} + + + +
+
+
+ +
+
+ + + +
+
+
+
+ ); +}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/MENU_DATA_VIEWER_AUTO_REFRESH.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/MENU_DATA_VIEWER_AUTO_REFRESH.ts new file mode 100644 index 0000000000..64fc2c33c1 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/MENU_DATA_VIEWER_AUTO_REFRESH.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ACTION_REFRESH, createMenu } from '@cloudbeaver/core-view'; + +export const MENU_DATA_VIEWER_AUTO_REFRESH = createMenu('auto-refresh', { + label: 'data_viewer_action_auto_refresh_menu_tooltip', + + tooltip: 'data_viewer_action_auto_refresh_menu_tooltip', + action: ACTION_REFRESH, +}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/RefreshMenuAction.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/RefreshMenuAction.tsx new file mode 100644 index 0000000000..4b86ec1d4d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/RefreshMenuAction.tsx @@ -0,0 +1,68 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import { forwardRef, useEffect } from 'react'; + +import { type CRegistryList, type IComponentsTreeNodeValidator, TimerIcon, useParentProps } from '@cloudbeaver/core-blocks'; +import { MenuBarAction, MenuBarItem, type MenuBarItemProps } from '@cloudbeaver/core-ui'; +import { ACTION_REFRESH } from '@cloudbeaver/core-view'; + +import { getRefreshState } from './getRefreshState.js'; + +const RefreshMenuItem: typeof MenuBarItem = observer( + forwardRef(function RefreshMenuItem(props, ref) { + const actionProps = useParentProps(MenuBarAction); + const refreshAction = actionProps ? getRefreshState(actionProps.context) : undefined; + + useEffect(() => { + refreshAction?.resume(); + + return () => { + refreshAction?.pause(); + }; + }, [refreshAction]); + + if (!actionProps || !refreshAction) { + return } />; + } + + let interval: number | string = Math.round(refreshAction.interval / 1000); + + if (interval > 99) { + interval = '99+'; + } + + return } />; + }), +); + +export const REFRESH_MENU_ITEM_REGISTRY: CRegistryList = [ + [ + MenuBarItem, + [ + { + component: MenuBarAction, + validator({ item, context }) { + if (item.action.action !== ACTION_REFRESH) { + return false; + } + const state = getRefreshState(context); + + return !!state?.isAutoRefresh; + }, + } as IComponentsTreeNodeValidator, + { + component: MenuBarItem, + replacement: RefreshMenuItem, + validator(props) { + return typeof props.icon === 'string'; + }, + } as IComponentsTreeNodeValidator, + ], + ], +]; diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/TableRefreshActionBootstrap.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/TableRefreshActionBootstrap.ts new file mode 100644 index 0000000000..eaf609c816 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/TableRefreshActionBootstrap.ts @@ -0,0 +1,206 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; + +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { type IDataContextProvider } from '@cloudbeaver/core-data-context'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { declensionOfNumber } from '@cloudbeaver/core-utils'; +import { ACTION_REFRESH, ActionService, MenuBaseItem, menuExtractItems, MenuSeparatorItem, MenuService } from '@cloudbeaver/core-view'; + +import { type IDatabaseRefreshState } from '../../../../DatabaseDataModel/Actions/DatabaseRefreshAction.js'; +import { DATA_CONTEXT_DV_DDM } from '../../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.js'; +import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.js'; +import { DATA_VIEWER_DATA_MODEL_ACTIONS_MENU } from '../DATA_VIEWER_DATA_MODEL_ACTIONS_MENU.js'; +import { getRefreshState } from './getRefreshState.js'; +import { MENU_DATA_VIEWER_AUTO_REFRESH } from './MENU_DATA_VIEWER_AUTO_REFRESH.js'; + +const AutoRefreshSettingsDialog = importLazyComponent(() => + import('./AutoRefreshSettingsDialog.js').then(module => module.AutoRefreshSettingsDialog), +); + +const AUTO_REFRESH_INTERVALS = [5, 10, 15, 30, 60]; + +@injectable(() => [ActionService, MenuService, LocalizationService, CommonDialogService]) +export class TableRefreshActionBootstrap extends Bootstrap { + constructor( + private readonly actionService: ActionService, + private readonly menuService: MenuService, + private readonly localizationService: LocalizationService, + private readonly commonDialogService: CommonDialogService, + ) { + super(); + } + + override register() { + this.registerGeneralActions(); + } + + private registerGeneralActions() { + this.menuService.addCreator({ + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isApplicable(context) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + return !model.isDisabled(resultIndex); + }, + getItems(context, items) { + return [MENU_DATA_VIEWER_AUTO_REFRESH, ...items]; + }, + orderItems(context, items) { + const extracted = menuExtractItems(items, [MENU_DATA_VIEWER_AUTO_REFRESH]); + return [...extracted, ...items]; + }, + }); + this.menuService.addCreator({ + menus: [MENU_DATA_VIEWER_AUTO_REFRESH], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isApplicable(context) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + return !model.isDisabled(resultIndex); + }, + getItems: (context, items) => { + const state = getRefreshState(context); + items = [...items]; + for (const interval of AUTO_REFRESH_INTERVALS) { + const label = this.getLabel(interval); + const tooltip = this.localizationService.translate('data_viewer_action_auto_refresh_interval_tooltip', undefined, { interval: label }); + items.push( + new MenuBaseItem( + { + id: `auto-refresh-${interval}`, + label, + tooltip, + disabled: state?.interval === interval * 1000, + }, + { + onSelect: () => { + state?.setInterval(interval * 1000); + }, + }, + ), + ); + } + + items.push(new MenuSeparatorItem()); + + items.push( + new MenuBaseItem( + { + id: 'auto-refresh-custom', + label: 'ui_custom', + tooltip: 'data_viewer_action_auto_refresh_menu_configure_tooltip', + }, + { + onSelect: this.configureAutoRefresh.bind(this, context), + }, + ), + ); + + items.push( + new MenuBaseItem( + { + id: 'auto-refresh-stop', + label: 'ui_processing_stop', + tooltip: 'data_viewer_action_auto_refresh_menu_stop_tooltip', + disabled: !state?.isAutoRefresh, + }, + { + onSelect: () => { + state?.setInterval(0); + }, + }, + ), + ); + + return items; + }, + }); + this.actionService.addHandler({ + id: 'data-base-handler', + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU, MENU_DATA_VIEWER_AUTO_REFRESH], + actions: [ACTION_REFRESH], + isDisabled(context) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + + return model.isLoading(); + }, + getActionInfo(context, action) { + if (action === ACTION_REFRESH) { + const state = getRefreshState(context); + return { + ...action.info, + icon: state?.isAutoRefresh ? '/icons/timer_m.svg#root' : '/icons/refresh_sm.svg', + label: '', + tooltip: state?.isAutoRefresh ? 'data_viewer_action_auto_refresh_stop_tooltip' : 'data_viewer_action_refresh_tooltip', + }; + } + + return action.info; + }, + handler: context => { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const state = getRefreshState(context); + + if (state?.isAutoRefresh) { + state.setInterval(0); + } else { + model.refresh(); + } + }, + }); + } + + private async configureAutoRefresh(context: IDataContextProvider) { + const state = getRefreshState(context); + if (!state) { + return; + } + + const stateCopy = observable({ + interval: state.interval / 1000, + paused: state.paused, + stopOnError: state.stopOnError, + }); + + const result = await this.commonDialogService.open(AutoRefreshSettingsDialog, { state: stateCopy }); + + if (result === DialogueStateResult.Resolved) { + let interval = stateCopy.interval; + + if (typeof interval === 'string') { + interval = Number.parseInt(interval); + } + + if (!Number.isInteger(interval)) { + interval = 0; + } + + state.setInterval(interval * 1000); + state.setStopOnError(stateCopy.stopOnError); + } + } + + private getLabel(interval: number) { + let message = ['ui_second_first_form', 'ui_second_second_form', 'ui_second_third_form']; + + if (interval >= 60) { + message = ['ui_minute_first_form', 'ui_minute_second_form', 'ui_minute_third_form']; + interval = Math.round(interval / 60); + } + + return this.localizationService.translate(declensionOfNumber(interval, message), undefined, { interval }); + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/getRefreshState.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/getRefreshState.ts new file mode 100644 index 0000000000..2ef13f1eb2 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/RefreshAction/getRefreshState.ts @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { type IDataContextProvider } from '@cloudbeaver/core-data-context'; + +import { DatabaseRefreshAction } from '../../../../DatabaseDataModel/Actions/DatabaseRefreshAction.js'; +import { DATA_CONTEXT_DV_DDM } from '../../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.js'; +import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.js'; +import { type IDatabaseDataResult } from '../../../../DatabaseDataModel/IDatabaseDataResult.js'; + +export function getRefreshState(context: IDataContextProvider): DatabaseRefreshAction | null { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + if (resultIndex === undefined || !model.source.hasResult(resultIndex)) { + return null; + } + return model.source.getAction(resultIndex, DatabaseRefreshAction); +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.m.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.m.css deleted file mode 100644 index ec052c096f..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.m.css +++ /dev/null @@ -1,4 +0,0 @@ -.wrapper { - display: flex; - height: 100%; -} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx index c672134122..237dc6c753 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx @@ -1,50 +1,46 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; +import type { HTMLAttributes } from 'react'; -import { s, useS } from '@cloudbeaver/core-blocks'; -import type { IDataContext } from '@cloudbeaver/core-data-context'; -import { useService } from '@cloudbeaver/core-di'; -import { MenuBar } from '@cloudbeaver/core-ui'; +import { CRegistry, s, useS } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; +import { MenuBar, MenuBarItemStyles, MenuBarStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; -import { DATA_CONTEXT_DV_DDM } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM'; -import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX'; -import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel'; -import { DATA_CONTEXT_DATA_VIEWER_SIMPLE } from '../../TableHeader/DATA_CONTEXT_DATA_VIEWER_SIMPLE'; -import { DATA_VIEWER_DATA_MODEL_ACTIONS_MENU } from './DATA_VIEWER_DATA_MODEL_ACTIONS_MENU'; -import style from './TableFooterMenu.m.css'; -import { TableFooterMenuItem } from './TableFooterMenuItem'; -import { TableFooterMenuService } from './TableFooterMenuService'; +import { DATA_CONTEXT_DV_DDM } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.js'; +import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.js'; +import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel.js'; +import { DATA_CONTEXT_DV_SIMPLE } from '../../DATA_CONTEXT_DV_SIMPLE.js'; +import { DATA_VIEWER_DATA_MODEL_ACTIONS_MENU } from './DATA_VIEWER_DATA_MODEL_ACTIONS_MENU.js'; +import { REFRESH_MENU_ITEM_REGISTRY } from './RefreshAction/RefreshMenuAction.js'; interface Props { resultIndex: number; - model: IDatabaseDataModel; + model: IDatabaseDataModel; simple: boolean; - context?: IDataContext; className?: string; + role?: HTMLAttributes['role']; } -export const TableFooterMenu = observer(function TableFooterMenu({ resultIndex, model, simple, context, className }) { - const mainMenuService = useService(TableFooterMenuService); - const styles = useS(style); - const menu = useMenu({ menu: DATA_VIEWER_DATA_MODEL_ACTIONS_MENU, context }); +export const TableFooterMenu = observer(function TableFooterMenu({ resultIndex, model, simple, role, className }) { + const styles = useS(MenuBarStyles, MenuBarItemStyles); + const menu = useMenu({ menu: DATA_VIEWER_DATA_MODEL_ACTIONS_MENU }); - menu.context.set(DATA_CONTEXT_DV_DDM, model); - menu.context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex); - menu.context.set(DATA_CONTEXT_DATA_VIEWER_SIMPLE, simple); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_DV_DDM, model, id); + context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex, id); + context.set(DATA_CONTEXT_DV_SIMPLE, simple, id); + }); return ( -
- {mainMenuService.constructMenuWithContext(model, resultIndex, simple).map((topItem, i) => ( - - ))} - -
+ + + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx deleted file mode 100644 index 5634c39415..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx +++ /dev/null @@ -1,86 +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 { observer } from 'mobx-react-lite'; -import type { ButtonHTMLAttributes } from 'react'; -import styled, { css, use } from 'reshadow'; - -import { IconOrImage, MenuTrigger, ToolsAction, useTranslate } from '@cloudbeaver/core-blocks'; -import type { IMenuItem } from '@cloudbeaver/core-dialogs'; - -type Props = ButtonHTMLAttributes & { - menuItem: IMenuItem; -}; - -export const tableFooterMenuStyles = css` - Menu { - composes: theme-text-on-surface from global; - } - MenuTrigger { - composes: theme-ripple from global; - height: 100%; - padding: 0 12px; - display: flex; - align-items: center; - cursor: pointer; - &[|hidden] { - display: none; - } - } - ToolsAction[|hidden] { - display: none; - } - menu-trigger-icon IconOrImage { - display: block; - width: 16px; - } - menu-trigger-title { - display: block; - } - menu-trigger-icon + menu-trigger-title { - padding-left: 8px; - } -`; - -export const TableFooterMenuItem = observer(function TableFooterMenuItem({ menuItem, ...props }) { - const translate = useTranslate(); - - if (!menuItem.panel) { - return styled(tableFooterMenuStyles)( - menuItem.onClick?.()} - > - {translate(menuItem.title)} - , - ); - } - - return styled(tableFooterMenuStyles)( - - {menuItem.icon && ( - - - - )} - {menuItem.title && {translate(menuItem.title)}} - , - ); -}); 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..70118919f2 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 @@ -1,282 +1,218 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { injectable } from '@cloudbeaver/core-di'; -import { ContextMenuService, IContextMenuItem, IMenuContext, IMenuItem } from '@cloudbeaver/core-dialogs'; - -import { DatabaseEditAction } from '../../../DatabaseDataModel/Actions/DatabaseEditAction'; -import { DatabaseSelectAction } from '../../../DatabaseDataModel/Actions/DatabaseSelectAction'; -import { DatabaseEditChangeType } from '../../../DatabaseDataModel/Actions/IDatabaseDataEditAction'; -import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel'; - -export interface ITableFooterMenuContext { - model: IDatabaseDataModel; - resultIndex: number; - simple: boolean; -} - -@injectable() +import { + ACTION_ADD, + ACTION_CANCEL, + ACTION_DELETE, + ACTION_DUPLICATE, + ACTION_REVERT, + ACTION_SAVE, + ActionService, + getBindingLabel, + KEY_BINDING_ADD, + KEY_BINDING_DUPLICATE, + MenuService, + type IAction, +} from '@cloudbeaver/core-view'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; + +import { DatabaseEditAction } from '../../../DatabaseDataModel/Actions/DatabaseEditAction.js'; +import { DatabaseSelectAction } from '../../../DatabaseDataModel/Actions/DatabaseSelectAction.js'; +import { DatabaseEditChangeType } from '../../../DatabaseDataModel/Actions/IDatabaseDataEditAction.js'; +import { DATA_CONTEXT_DV_DDM } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.js'; +import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.js'; +import { DATA_CONTEXT_DV_PRESENTATION, DataViewerPresentationType } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_PRESENTATION.js'; +import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel.js'; +import { DATA_VIEWER_DATA_MODEL_ACTIONS_MENU } from './DATA_VIEWER_DATA_MODEL_ACTIONS_MENU.js'; + +@injectable(() => [ActionService, LocalizationService, MenuService]) export class TableFooterMenuService { - static nodeContextType = 'NodeWithParent'; - private readonly tableFooterMenuToken = 'tableFooterMenu'; - - constructor(private readonly contextMenuService: ContextMenuService) { - this.contextMenuService.addPanel(this.tableFooterMenuToken); - - this.registerMenuItem({ - id: 'table_add', - order: 0.5, - icon: '/icons/data_add_sm.svg', - tooltip: 'data_viewer_action_edit_add', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - if (context.data.model.isReadonly(context.data.resultIndex)) { - return true; - } + constructor( + private readonly actionService: ActionService, + private readonly localizationService: LocalizationService, + private readonly menuService: MenuService, + ) { } + + register(): void { + this.registerEditingActions(); + } - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + private registerEditingActions() { + this.menuService.addCreator({ + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isApplicable(context) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION); - return !editor?.hasFeature('add'); + return !model.isReadonly(resultIndex) && !presentation?.readonly && (!presentation || presentation.type === DataViewerPresentationType.Data); }, - isDisabled(context) { - return ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ); - }, - onClick(context) { - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - - if (!editor) { - return; - } - - const select = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseSelectAction); - - editor.add(select?.getFocusedElement()); + getItems(context, items) { + return [ACTION_ADD, ACTION_DUPLICATE, ACTION_DELETE, ACTION_REVERT, ACTION_SAVE, ACTION_CANCEL, ...items]; }, }); - this.registerMenuItem({ - id: 'table_add_copy', - order: 0.55, - icon: '/icons/data_add_copy_sm.svg', - tooltip: 'data_viewer_action_edit_add_copy', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - if (context.data.model.isReadonly(context.data.resultIndex)) { - return true; + this.actionService.addHandler({ + id: 'data-base-editing-handler', + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU], + actions: [ACTION_ADD, ACTION_DUPLICATE, ACTION_DELETE, ACTION_REVERT, ACTION_SAVE, ACTION_CANCEL], + isActionApplicable(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + if (model.isReadonly(resultIndex)) { + return false; } - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - - return !editor?.hasFeature('add'); - }, - isDisabled(context) { - if ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ) { - return true; - } - - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); - - return selectedElements.length === 0; - }, - onClick(context) { - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); if (!editor) { - return; + return false; } - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); - - editor.duplicate(...selectedElements); - }, - }); - this.registerMenuItem({ - id: 'table_delete', - order: 0.6, - icon: '/icons/data_delete_sm.svg', - tooltip: 'data_viewer_action_edit_delete', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - if (context.data.model.isReadonly(context.data.resultIndex)) { - return true; + switch (action) { + case ACTION_DUPLICATE: + case ACTION_ADD: { + return editor.hasFeature('add'); + } + case ACTION_DELETE: { + return editor.hasFeature('delete'); + } + case ACTION_REVERT: { + return editor.hasFeature('revert'); + } } - - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - - return !editor?.hasFeature('delete'); + return true; }, - isDisabled(context) { - if ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ) { - return true; - } - - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + isDisabled(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; - if (!editor) { + if (model.isLoading() || model.isDisabled(resultIndex) || !model.source.getResult(resultIndex)) { return true; } - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); + switch (action) { + case ACTION_DUPLICATE: { + const selectedElements = getActiveElements(model, resultIndex); - if (selectedElements.length === 0) { - return true; - } - - return !selectedElements.some(key => editor.getElementState(key) !== DatabaseEditChangeType.delete); - }, - onClick(context) { - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); - - editor?.delete(...selectedElements); - }, - }); - this.registerMenuItem({ - id: 'table_revert', - order: 0.7, - icon: '/icons/data_revert_sm.svg', - tooltip: 'data_viewer_action_edit_revert', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - if (context.data.model.isReadonly(context.data.resultIndex)) { - return true; - } + return selectedElements.length === 0; + } + case ACTION_DELETE: { + const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); + const selectedElements = getActiveElements(model, resultIndex); - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + const canEdit = + model.hasElementIdentifier(resultIndex) || selectedElements.every(key => editor?.getElementState(key) === DatabaseEditChangeType.add); - return !editor; - }, - isDisabled(context) { - if ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ) { - return true; - } + if (!editor || !canEdit) { + return true; + } - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + return selectedElements.length === 0 || !selectedElements.some(key => editor.getElementState(key) !== DatabaseEditChangeType.delete); + } + case ACTION_REVERT: { + const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); + if (!editor) { + return true; + } - return ( - !editor || - selectedElements.length === 0 || - !selectedElements.some(key => { - const state = editor.getElementState(key); + const selectedElements = getActiveElements(model, resultIndex); - if (state === DatabaseEditChangeType.add) { - return editor.isElementEdited(key); - } + return ( + selectedElements.length === 0 || + !selectedElements.some(key => { + const state = editor.getElementState(key); - return state !== null; - }) - ); - }, - onClick(context) { - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + if (state === DatabaseEditChangeType.add) { + return editor.isElementEdited(key); + } - const selectedElements = getActiveElements(context.data.model, context.data.resultIndex); + return state !== null; + }) + ); + } + case ACTION_SAVE: + case ACTION_CANCEL: { + const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); - editor?.revert(...selectedElements); - }, - }); - this.registerMenuItem({ - id: 'save ', - order: 1, - title: 'ui_processing_save', - tooltip: 'ui_processing_save', - icon: 'table-save', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - return context.data.model.isReadonly(context.data.resultIndex); - }, - isDisabled(context) { - if ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ) { - return true; + return !editor?.isEdited(); + } } - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - - return !editor?.isEdited(); + return false; }, - onClick: context => context.data.model.save(), + getActionInfo: this.tableFooterMenuGetActionInfo.bind(this), + handler: this.tableFooterMenuActionHandler.bind(this), }); + } - this.registerMenuItem({ - id: 'cancel ', - order: 2, - title: 'data_viewer_value_revert', - tooltip: 'data_viewer_value_revert_title', - icon: '/icons/data_revert_all_sm.svg', - isPresent(context) { - return context.contextType === TableFooterMenuService.nodeContextType; - }, - isHidden(context) { - return context.data.model.isReadonly(context.data.resultIndex); - }, - isDisabled(context) { - if ( - context.data.model.isLoading() || - context.data.model.isDisabled(context.data.resultIndex) || - !context.data.model.source.hasResult(context.data.resultIndex) - ) { - return true; - } + private tableFooterMenuActionHandler(context: IDataContextProvider, action: IAction) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); + if (!editor) { + return; + } + const select = model.source.getActionImplementation(resultIndex, DatabaseSelectAction); + const selectedElements = getActiveElements(model, resultIndex); - return !editor?.isEdited(); - }, - onClick: context => { - const editor = context.data.model.source.getActionImplementation(context.data.resultIndex, DatabaseEditAction); - editor?.clear(); - }, - }); - } + switch (action) { + case ACTION_ADD: { + editor.add(select?.getFocusedElement()); + break; + } + case ACTION_DUPLICATE: { + editor.duplicate(...selectedElements); + break; + } + case ACTION_DELETE: { + editor.delete(...selectedElements); + break; + } + case ACTION_REVERT: { + editor.revert(...selectedElements); + break; + } + case ACTION_SAVE: + model.save().catch(() => { }); + break; + case ACTION_CANCEL: { + editor.clear(); + break; + } + } - constructMenuWithContext(model: IDatabaseDataModel, resultIndex: number, simple: boolean): IMenuItem[] { - const context: IMenuContext = { - menuId: this.tableFooterMenuToken, - contextId: model.id, - contextType: TableFooterMenuService.nodeContextType, - data: { model, resultIndex, simple }, - }; - return this.contextMenuService.createContextMenu(context, this.tableFooterMenuToken).menuItems; } - registerMenuItem(options: IContextMenuItem): void { - this.contextMenuService.addMenuItem(this.tableFooterMenuToken, options); + private tableFooterMenuGetActionInfo(context: IDataContextProvider, action: IAction) { + const t = this.localizationService.translate; + switch (action) { + case ACTION_ADD: + return { ...action.info, label: '', icon: '/icons/data_add_sm.svg', tooltip: t('data_viewer_action_edit_add') + ' (' + getBindingLabel(KEY_BINDING_ADD) + ')' }; + case ACTION_DUPLICATE: + return { ...action.info, label: '', icon: '/icons/data_add_copy_sm.svg', tooltip: t('data_viewer_action_edit_add_copy') + ' (' + getBindingLabel(KEY_BINDING_DUPLICATE) + ')' }; + case ACTION_DELETE: + return { ...action.info, label: '', icon: '/icons/data_delete_sm.svg', tooltip: t('data_viewer_action_edit_delete') }; + case ACTION_REVERT: + return { ...action.info, label: '', icon: '/icons/data_revert_sm.svg', tooltip: t('data_viewer_action_edit_revert') }; + case ACTION_SAVE: + return { ...action.info, icon: 'table-save' }; + case ACTION_CANCEL: + return { ...action.info, icon: '/icons/data_revert_all_sm.svg', tooltip: t('data_viewer_value_revert_title') }; + } + + return action.info; } } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx deleted file mode 100644 index a0a8684f62..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableGrid.tsx +++ /dev/null @@ -1,54 +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 { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; - -import { TextPlaceholder } from '@cloudbeaver/core-blocks'; -import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; - -import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; -import type { IDataPresentationOptions } from '../DataPresentationService'; -import type { IDataTableActions } from './IDataTableActions'; -import { TableStatistics } from './TableStatistics'; - -interface Props { - model: IDatabaseDataModel; - actions: IDataTableActions; - dataFormat: ResultDataFormat; - presentation: IDataPresentationOptions; - resultIndex: number; - simple: boolean; -} - -const styles = css` - Presentation { - flex: 1; - overflow: auto; - } -`; - -export const TableGrid = observer(function TableGrid({ model, actions, dataFormat, presentation, resultIndex, simple }) { - if ((presentation.dataFormat !== undefined && dataFormat !== presentation.dataFormat) || !model.source.hasResult(resultIndex)) { - if (model.isLoading()) { - return null; - } - - // eslint-disable-next-line react/no-unescaped-entities - return Current data can't be displayed by selected presentation; - } - - const result = model.getResult(resultIndex); - - const Presentation = presentation.getPresentationComponent(); - - if (result?.loadedFully && !result.data) { - return ; - } - - return styled(styles)(); -}); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_CONTEXT_DATA_VIEWER_SIMPLE.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_CONTEXT_DATA_VIEWER_SIMPLE.ts deleted file mode 100644 index 5fb848270c..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_CONTEXT_DATA_VIEWER_SIMPLE.ts +++ /dev/null @@ -1,10 +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 { createDataContext } from '@cloudbeaver/core-data-context'; - -export const DATA_CONTEXT_DATA_VIEWER_SIMPLE = createDataContext('data-viewer-database-simple'); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_VIEWER_DATA_MODEL_TOOLS_MENU.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_VIEWER_DATA_MODEL_TOOLS_MENU.ts index 07a1fb85f5..ef75870e35 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_VIEWER_DATA_MODEL_TOOLS_MENU.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/DATA_VIEWER_DATA_MODEL_TOOLS_MENU.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const DATA_VIEWER_DATA_MODEL_TOOLS_MENU = createMenu('data-viewer-data-model-tools', 'Data viewer data model tools menu'); +export const DATA_VIEWER_DATA_MODEL_TOOLS_MENU = createMenu('data-viewer-data-model-tools', { label: 'Data Editor data model tools menu' }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.module.css new file mode 100644 index 0000000000..6f57766da3 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.module.css @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tableHeader { + flex: 0 0 auto; + display: flex; + align-items: center; + + &:empty { + display: none; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx index 44c8d8c43c..dc53645fed 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx @@ -1,44 +1,35 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; -import { Placeholder } from '@cloudbeaver/core-blocks'; +import { Placeholder, s, useS } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; -import { TableHeaderService } from './TableHeaderService'; - -const styles = css` - table-header { - flex: 0 0 auto; - display: flex; - align-items: center; - - &:empty { - display: none; - } - } -`; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import classes from './TableHeader.module.css'; +import { TableHeaderService } from './TableHeaderService.js'; interface Props { - model: IDatabaseDataModel; + model: IDatabaseDataModel; resultIndex: number; simple: boolean; className?: string; + tabIndex?: number; + 'data-table-header'?: boolean; } -export const TableHeader = observer(function TableHeader({ model, resultIndex, simple, className }) { +export const TableHeader = observer(function TableHeader({ model, resultIndex, simple, className, ...rest }) { + const styles = useS(classes); const service = useService(TableHeaderService); - return styled(styles)( - + return ( +
- , +
); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderMenu.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderMenu.tsx index ed52b16f12..a3484af4e9 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderMenu.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderMenu.tsx @@ -1,29 +1,32 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { PlaceholderComponent, useS } from '@cloudbeaver/core-blocks'; +import { type PlaceholderComponent, useS } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { MenuBar, MenuBarItemStyles, MenuBarStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; -import { DATA_CONTEXT_DV_DDM } from '../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM'; -import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX'; -import { DATA_CONTEXT_DATA_VIEWER_SIMPLE } from './DATA_CONTEXT_DATA_VIEWER_SIMPLE'; -import { DATA_VIEWER_DATA_MODEL_TOOLS_MENU } from './DATA_VIEWER_DATA_MODEL_TOOLS_MENU'; -import type { ITableHeaderPlaceholderProps } from './TableHeaderService'; +import { DATA_CONTEXT_DV_DDM } from '../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.js'; +import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.js'; +import { DATA_CONTEXT_DV_SIMPLE } from '../DATA_CONTEXT_DV_SIMPLE.js'; +import { DATA_VIEWER_DATA_MODEL_TOOLS_MENU } from './DATA_VIEWER_DATA_MODEL_TOOLS_MENU.js'; +import type { ITableHeaderPlaceholderProps } from './TableHeaderService.js'; export const TableHeaderMenu: PlaceholderComponent = observer(function TableHeaderMenu({ model, simple, resultIndex }) { const menu = useMenu({ menu: DATA_VIEWER_DATA_MODEL_TOOLS_MENU }); const menuBarStyles = useS(MenuBarStyles, MenuBarItemStyles); - menu.context.set(DATA_CONTEXT_DV_DDM, model); - menu.context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex); - menu.context.set(DATA_CONTEXT_DATA_VIEWER_SIMPLE, simple); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_DV_DDM, model, id); + context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, resultIndex, id); + context.set(DATA_CONTEXT_DV_SIMPLE, simple, id); + }); - return ; + return ; }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts index db37b5c963..a1d17ca43d 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -11,48 +11,52 @@ import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view'; -import { DATA_VIEWER_CONSTRAINTS_DELETE_ACTION } from '../../DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_CONSTRAINTS_DELETE_ACTION'; -import { ResultSetConstraintAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction'; -import { DATA_CONTEXT_DV_DDM } from '../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM'; -import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX'; -import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; -import { DATA_VIEWER_DATA_MODEL_TOOLS_MENU } from './DATA_VIEWER_DATA_MODEL_TOOLS_MENU'; +import { DatabaseDataConstraintAction } from '../../DatabaseDataModel/Actions/DatabaseDataConstraintAction.js'; +import { DATA_VIEWER_CONSTRAINTS_DELETE_ACTION } from '../../DatabaseDataModel/Actions/ResultSet/Actions/DATA_VIEWER_CONSTRAINTS_DELETE_ACTION.js'; +import { DATA_CONTEXT_DV_DDM } from '../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.js'; +import { DATA_CONTEXT_DV_DDM_RESULT_INDEX } from '../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.js'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import { isResultSetDataSource, ResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; +import { DATA_VIEWER_DATA_MODEL_TOOLS_MENU } from './DATA_VIEWER_DATA_MODEL_TOOLS_MENU.js'; export const TableWhereFilter = React.lazy(async () => { - const { TableWhereFilter } = await import('./TableWhereFilter'); + const { TableWhereFilter } = await import('./TableWhereFilter.js'); return { default: TableWhereFilter }; }); export const TableHeaderMenu = React.lazy(async () => { - const { TableHeaderMenu } = await import('./TableHeaderMenu'); + const { TableHeaderMenu } = await import('./TableHeaderMenu.js'); return { default: TableHeaderMenu }; }); export interface ITableHeaderPlaceholderProps { - model: IDatabaseDataModel; + model: IDatabaseDataModel; resultIndex: number; simple: boolean; } -@injectable() +@injectable(() => [MenuService, ActionService]) export class TableHeaderService extends Bootstrap { readonly tableHeaderPlaceholder = new PlaceholderContainer(); - constructor(private readonly menuService: MenuService, private readonly actionService: ActionService) { + constructor( + private readonly menuService: MenuService, + private readonly actionService: ActionService, + ) { super(); } - register(): void { - this.tableHeaderPlaceholder.add(TableWhereFilter, 1); + override register(): void { + this.tableHeaderPlaceholder.add(TableWhereFilter, 1, props => !isResultSetDataSource(props.model.source)); this.tableHeaderPlaceholder.add(TableHeaderMenu, 2); this.actionService.addHandler({ id: 'table-header-menu-base-handler', - isActionApplicable(context, action) { + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + isActionApplicable(context) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; const menu = context.hasValue(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_TOOLS_MENU); - const model = context.tryGet(DATA_CONTEXT_DV_DDM); - const resultIndex = context.tryGet(DATA_CONTEXT_DV_DDM_RESULT_INDEX); - if (!menu || !model || resultIndex === undefined) { + if (!menu || !isResultSetDataSource(model.source)) { return false; } @@ -61,9 +65,9 @@ export class TableHeaderService extends Bootstrap { handler: async (context, action) => { switch (action) { case DATA_VIEWER_CONSTRAINTS_DELETE_ACTION: { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); - const constraints = model.source.tryGetAction(resultIndex, ResultSetConstraintAction); + const model = context.get(DATA_CONTEXT_DV_DDM)! as unknown as IDatabaseDataModel; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const constraints = model.source.tryGetAction(resultIndex, DatabaseDataConstraintAction); if (constraints) { constraints.deleteData(); @@ -81,15 +85,15 @@ export class TableHeaderService extends Bootstrap { return action.info; }, isDisabled: (context, action) => { - const model = context.get(DATA_CONTEXT_DV_DDM); - const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX); + const model = context.get(DATA_CONTEXT_DV_DDM)! as unknown as IDatabaseDataModel; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; if (model.isLoading() || model.isDisabled(resultIndex)) { return true; } if (action === DATA_VIEWER_CONSTRAINTS_DELETE_ACTION) { - const constraints = model.source.tryGetAction(resultIndex, ResultSetConstraintAction); + const constraints = model.source.tryGetAction(resultIndex, DatabaseDataConstraintAction); if (model.source.options?.whereFilter) { return false; @@ -105,10 +109,8 @@ export class TableHeaderService extends Bootstrap { }); this.menuService.addCreator({ - isApplicable: context => context.get(DATA_CONTEXT_MENU) === DATA_VIEWER_DATA_MODEL_TOOLS_MENU, + menus: [DATA_VIEWER_DATA_MODEL_TOOLS_MENU], getItems: (context, items) => [...items, DATA_VIEWER_CONSTRAINTS_DELETE_ACTION], }); } - - load(): void | Promise {} } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.module.css new file mode 100644 index 0000000000..a48decf5e2 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.module.css @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.inlineEditor { + composes: theme-background-surface theme-text-on-surface from global; +} + +.imbeddedEditor .inlineEditor { + flex: 1; + height: 24px; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx index a9cb264328..8af46221c3 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx @@ -1,43 +1,80 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; +import { useRef } from 'react'; -import { PlaceholderComponent, useTranslate } from '@cloudbeaver/core-blocks'; +import { + Container, + type InputAutocompleteProposal, + InputAutocompletionMenu, + type PlaceholderComponent, + useInputAutocomplete, + useTranslate, +} from '@cloudbeaver/core-blocks'; import { InlineEditor } from '@cloudbeaver/core-ui'; -import type { ITableHeaderPlaceholderProps } from './TableHeaderService'; -import { useWhereFilter } from './useWhereFilter'; +import type { ITableHeaderPlaceholderProps } from './TableHeaderService.js'; +import styles from './TableWhereFilter.module.css'; +import { useTableViewerHeaderData } from './useTableViewerHeaderData.js'; +import { useWhereFilter } from './useWhereFilter.js'; -const styles = css` - InlineEditor { - composes: theme-background-surface theme-text-on-surface from global; - flex: 1; - height: 24px; - } -`; +const AUTOCOMPLETE_WORD_SEPARATOR = /[^\w]/; export const TableWhereFilter: PlaceholderComponent = observer(function TableWhereFilter({ model, resultIndex }) { const translate = useTranslate(); const state = useWhereFilter(model, resultIndex); + const data = useTableViewerHeaderData({ model, resultIndex }); + const inputRef = useRef(null); + const autocompleteState = useInputAutocomplete(inputRef, { + separator: AUTOCOMPLETE_WORD_SEPARATOR, + sourceHints: data.hintProposals ?? [], + matchStrategy: 'fuzzy', + }); + + if (!state.supported) { + return null; + } + + function handleSelect(proposal: InputAutocompleteProposal) { + autocompleteState.replaceCurrentWord(proposal.replacementString); + state.set(autocompleteState.inputValue); + } + + async function onSave() { + try { + await state.apply(); + } finally { + inputRef.current?.focus(); + } + } - return styled(styles)( - , + return ( + + + + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/useTableViewerHeaderData.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/useTableViewerHeaderData.ts new file mode 100644 index 0000000000..7cec7fc87d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/useTableViewerHeaderData.ts @@ -0,0 +1,104 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { computed } from 'mobx'; + +import { type InputAutocompleteProposal, useObservableRef } from '@cloudbeaver/core-blocks'; +import type { SqlResultColumn } from '@cloudbeaver/core-sdk'; + +import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.js'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import { isResultSetDataModel } from '../../ResultSet/isResultSetDataModel.js'; + +interface Props { + model: IDatabaseDataModel; + resultIndex: number; +} + +interface IState { + readonly columns: SqlResultColumn[]; + readonly hintProposals: InputAutocompleteProposal[]; +} + +const BASE_HINTS: InputAutocompleteProposal[] = [ + { + displayString: 'AND', + replacementString: 'AND', + }, + { + displayString: 'OR', + replacementString: 'OR', + }, + { + displayString: 'ILIKE', + replacementString: 'ILIKE', + }, + { + displayString: 'LIKE', + replacementString: 'LIKE', + }, + { + displayString: 'IN', + replacementString: 'IN', + }, + { + displayString: 'BETWEEN', + replacementString: 'BETWEEN', + }, + { + displayString: 'IS', + replacementString: 'IS', + }, + { + displayString: 'NOT', + replacementString: 'NOT', + }, + { + displayString: 'NULL', + replacementString: 'NULL', + }, +]; + +export function useTableViewerHeaderData({ model, resultIndex }: Props): Readonly { + return useObservableRef( + () => ({ + get columns() { + const model = this.model as any; + + if (!model.source.hasResult(this.resultIndex) || !isResultSetDataModel(model)) { + return []; + } + + const view = model.source.tryGetAction(resultIndex, ResultSetViewAction); + + if (!view) { + return []; + } + + return view?.columns ?? []; + }, + get hintProposals() { + return [...BASE_HINTS].concat( + this.columns.map(column => ({ + title: column.label || '', + displayString: column.label || '', + replacementString: column.label || '', + icon: column.icon || '', + })), + ); + }, + }), + { + columns: computed, + hintProposals: computed, + }, + { + resultIndex, + model, + }, + ); +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/useWhereFilter.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/useWhereFilter.ts index f5336259cf..49c9a63691 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/useWhereFilter.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/useWhereFilter.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,47 +9,60 @@ import { action, computed, observable } from 'mobx'; import { useObservableRef } from '@cloudbeaver/core-blocks'; -import { ResultSetConstraintAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction'; -import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; -import type { IDatabaseDataOptions } from '../../DatabaseDataModel/IDatabaseDataOptions'; +import { DatabaseDataConstraintAction } from '../../DatabaseDataModel/Actions/DatabaseDataConstraintAction.js'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import type { IDatabaseDataOptions } from '../../DatabaseDataModel/IDatabaseDataOptions.js'; +import { isResultSetDataModel } from '../../ResultSet/isResultSetDataModel.js'; +import { isResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; interface IState { - model: IDatabaseDataModel; + model: IDatabaseDataModel; resultIndex: number; + readonly supported: boolean; readonly filter: string; - readonly constraints: ResultSetConstraintAction | null; + readonly constraints: DatabaseDataConstraintAction | null; readonly disabled: boolean; readonly applicableFilter: boolean; set: (value: string) => void; apply: () => Promise; } -export function useWhereFilter(model: IDatabaseDataModel, resultIndex: number): Readonly { +export function useWhereFilter(model: IDatabaseDataModel, resultIndex: number): Readonly { return useObservableRef( () => ({ + get supported() { + return isResultSetDataSource(this.model.source); + }, get filter() { + const source = this.model.source; + if (!isResultSetDataSource(source)) { + return ''; + } + if (this.constraints?.filterConstraints.length && this.model.source.requestInfo.requestFilter) { return this.model.requestInfo.requestFilter; } - return this.model.source.options?.whereFilter ?? ''; + return source.options?.whereFilter ?? ''; }, get constraints() { - if (!this.model.source.hasResult(this.resultIndex)) { + const model = this.model as any; + if (!model.source.hasResult(this.resultIndex) || !isResultSetDataModel(model)) { return null; } - return this.model.source.tryGetAction(this.resultIndex, ResultSetConstraintAction) ?? null; + return model.source.tryGetAction(this.resultIndex, DatabaseDataConstraintAction) ?? null; }, get disabled() { const supported = this.constraints?.supported ?? false; return !supported || this.model.isLoading() || this.model.isDisabled(resultIndex); }, get applicableFilter() { - return ( - this.model.source.prevOptions?.whereFilter !== this.model.source.options?.whereFilter || - this.model.source.options?.whereFilter !== this.model.source.requestInfo.requestFilter - ); + const source = this.model.source; + if (!isResultSetDataSource(source)) { + return false; + } + return source.prevOptions?.whereFilter !== source.options?.whereFilter || source.options?.whereFilter !== source.requestInfo.requestFilter; }, set(value: string) { if (!this.constraints) { diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/PresentationTab.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/PresentationTab.tsx index ffb041bcdf..30501b2554 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/PresentationTab.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/PresentationTab.tsx @@ -1,53 +1,41 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled from 'reshadow'; -import { useStyles, useTranslate } from '@cloudbeaver/core-blocks'; -import type { ComponentStyle } from '@cloudbeaver/core-theming'; -import { BASE_TAB_STYLES, Tab, TabIcon, TabTitle, VERTICAL_ROTATED_TAB_STYLES } from '@cloudbeaver/core-ui'; +import { useTranslate } from '@cloudbeaver/core-blocks'; +import { Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; -import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; -import type { IDataPresentationOptions } from '../../DataPresentationService'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import type { IDataPresentationOptions } from '../../DataPresentationService.js'; interface Props { model: IDatabaseDataModel; resultIndex: number; presentation: IDataPresentationOptions; className?: string; - style?: ComponentStyle; onClick: (tabId: string) => void; } -export const PresentationTab = observer(function PresentationTab({ model, presentation, className, style, onClick }) { +export const PresentationTab = observer(function PresentationTab({ model, presentation, className, onClick }) { const translate = useTranslate(); - const styles = useStyles(BASE_TAB_STYLES, VERTICAL_ROTATED_TAB_STYLES, style); if (presentation.getTabComponent) { const Tab = presentation.getTabComponent(); return ( - + ); } - return styled(styles)( - + return ( + {presentation.icon && } {presentation.title && {translate(presentation.title)}} - , + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/TablePresentationBar.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/TablePresentationBar.tsx index afe54e1dff..89251c505a 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/TablePresentationBar.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/TablePresentationBar.tsx @@ -1,44 +1,26 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled, { css, use } from 'reshadow'; +import type { HTMLAttributes } from 'react'; -import { useStyles } from '@cloudbeaver/core-blocks'; +import { s, SContext, type StyleRegistry, useS } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { BASE_TAB_STYLES, TabList, TabsState, VERTICAL_ROTATED_TAB_STYLES } from '@cloudbeaver/core-ui'; +import { TabList, TabListStyles, TabsState, TabStyles } from '@cloudbeaver/core-ui'; -import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; -import { DataPresentationService, DataPresentationType } from '../../DataPresentationService'; -import { PresentationTab } from './PresentationTab'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import { DataPresentationService, DataPresentationType } from '../../DataPresentationService.js'; +import { PresentationTab } from './PresentationTab.js'; +import styles from './shared/TablePresentationBar.module.css'; +import TablePresentationBarTab from './shared/TablePresentationBarTab.module.css'; +import TablePresentationBarTabList from './shared/TablePresentationBarTabList.module.css'; -const styles = css` - table-left-bar { - display: flex; - } - Tab { - composes: theme-ripple theme-background-background theme-text-text-primary-on-light theme-typography--body2 from global; - text-transform: uppercase; - font-weight: normal; - - &:global([aria-selected='true']) { - font-weight: normal !important; - } - } - TabList { - composes: theme-background-secondary theme-text-on-secondary from global; - } - TabList[|flexible] tab-outer:only-child { - display: none; - } -`; - -interface Props { +interface Props extends HTMLAttributes { type: DataPresentationType; presentationId: string | null | undefined; dataFormat: ResultDataFormat; @@ -50,6 +32,11 @@ interface Props { onClose?: () => void; } +const tablePresentationBarRegistry: StyleRegistry = [ + [TabListStyles, { mode: 'append', styles: [TablePresentationBarTabList] }], + [TabStyles, { mode: 'append', styles: [TablePresentationBarTab] }], +]; + export const TablePresentationBar = observer(function TablePresentationBar({ type, presentationId, @@ -60,8 +47,9 @@ export const TablePresentationBar = observer(function TablePresentationBa className, onPresentationChange, onClose, + ...rest }) { - const style = useStyles(styles, BASE_TAB_STYLES, VERTICAL_ROTATED_TAB_STYLES); + const style = useS(styles); const dataPresentationService = useService(DataPresentationService); const presentations = dataPresentationService.getSupportedList(type, supportedDataFormat, dataFormat, model, resultIndex); const Tab = PresentationTab; // alias for styles matching @@ -79,15 +67,17 @@ export const TablePresentationBar = observer(function TablePresentationBa return null; } - return styled(style)( - + return ( +
- - {presentations.map(presentation => ( - - ))} - + + + {presentations.map(presentation => ( + + ))} + + - , +
); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/shared/TablePresentationBar.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/shared/TablePresentationBar.module.css new file mode 100644 index 0000000000..a20e8a4ba4 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/shared/TablePresentationBar.module.css @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tableLeftBar { + overflow: auto; +} + +.tabListFlexible .tabOuter:only-child { + display: none; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/shared/TablePresentationBarTab.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/shared/TablePresentationBarTab.module.css new file mode 100644 index 0000000000..b9475c5b41 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/shared/TablePresentationBarTab.module.css @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tab { + composes: theme-ripple theme-background-background theme-text-text-primary-on-light theme-typography--body2 from global; + text-transform: uppercase; + font-weight: normal; + + &:global([aria-selected='true']) { + font-weight: normal !important; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/shared/TablePresentationBarTabList.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/shared/TablePresentationBarTabList.module.css new file mode 100644 index 0000000000..daff16ec73 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TablePresentationBar/shared/TablePresentationBarTabList.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tabList { + composes: theme-background-secondary theme-text-on-secondary from global; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableStatistics.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableStatistics.module.css new file mode 100644 index 0000000000..13a4311fef --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableStatistics.module.css @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.statistics { + composes: theme-typography--caption from global; + flex: 1; + overflow: auto; + box-sizing: border-box; + white-space: pre-wrap; + padding: 16px; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableStatistics.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableStatistics.tsx index ad748ff3ac..de6a607309 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableStatistics.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableStatistics.tsx @@ -1,48 +1,50 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; +import type { HTMLAttributes } from 'react'; -import { useTranslate } from '@cloudbeaver/core-blocks'; +import { s, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; +import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel.js'; +import { type IDatabaseResultSet } from '../DatabaseDataModel/IDatabaseResultSet.js'; +import { isResultSetDataSource, ResultSetDataSource } from '../ResultSet/ResultSetDataSource.js'; +import classes from './TableStatistics.module.css'; -interface Props { +interface Props extends HTMLAttributes { model: IDatabaseDataModel; resultIndex: number; } -const styles = css` - statistics { - composes: theme-typography--caption from global; - flex: 1; - overflow: auto; - box-sizing: border-box; - white-space: pre-wrap; - padding: 16px; - } -`; - -export const TableStatistics = observer(function TableStatistics({ model, resultIndex }) { +export const TableStatistics = observer(function TableStatistics({ model, resultIndex, ...rest }) { + const styles = useS(classes); const translate = useTranslate(); const source = model.source; - const result = model.getResult(resultIndex); + let updatedRows: number | null = null; - return styled(styles)( - - {translate('data_viewer_statistics_status')} {source.requestInfo.requestMessage} -
- {translate('data_viewer_statistics_duration')} {source.requestInfo.requestDuration} ms + if (isResultSetDataSource(source)) { + const result = (source as ResultSetDataSource).getResult(resultIndex) as IDatabaseResultSet | null; + updatedRows = result?.updateRowCount ?? null; + } + + return ( +
+ {translate('data_viewer_statistics_status')} {translate(source.requestInfo.requestMessage)}
- {translate('data_viewer_statistics_updated_rows')} {result?.updateRowCount || 0} + {translate('data_viewer_statistics_duration')} {source.requestInfo.requestDuration} {translate('ui_ms')}
+ {updatedRows !== null && ( + <> + {translate('data_viewer_statistics_updated_rows')} {updatedRows} +
+ + )}
{source.requestInfo.source}
- , +
); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.module.css new file mode 100644 index 0000000000..79cb87d56d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.presentation { + flex: 1; + overflow: auto; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx index f58af5d320..1b18470f15 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableToolsPanel.tsx @@ -1,22 +1,22 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; -import { TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks'; +import { s, TextPlaceholder, useS, useTranslate } from '@cloudbeaver/core-blocks'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; -import type { IDataPresentationOptions } from '../DataPresentationService'; -import type { IDataTableActions } from './IDataTableActions'; +import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel.js'; +import type { IDataPresentationOptions } from '../DataPresentationService.js'; +import type { IDataTableActions } from './IDataTableActions.js'; +import styles from './TableToolsPanel.module.css'; interface Props { - model: IDatabaseDataModel; + model: IDatabaseDataModel; actions: IDataTableActions; dataFormat: ResultDataFormat; presentation: IDataPresentationOptions | null; @@ -24,17 +24,11 @@ interface Props { simple: boolean; } -const styles = css` - Presentation { - flex: 1; - overflow: auto; - } -`; - export const TableToolsPanel = observer(function TableToolsPanel({ model, actions, dataFormat, presentation, resultIndex, simple }) { const translate = useTranslate(); + const style = useS(styles); - const result = model.getResult(resultIndex); + const result = model.source.getResult(resultIndex); if (!presentation || (presentation.dataFormat !== undefined && dataFormat !== presentation.dataFormat)) { if (model.isLoading()) { @@ -51,5 +45,14 @@ export const TableToolsPanel = observer(function TableToolsPanel({ model, return {translate('data_viewer_nodata_message')}; } - return styled(styles)(); + return ( + + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.m.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.m.css deleted file mode 100644 index 089c343f8a..0000000000 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.m.css +++ /dev/null @@ -1,57 +0,0 @@ -.split { - &:not(.disabled) { - gap: 8px; - } -} - -.paneContent { - composes: theme-background-surface theme-text-on-surface from global; - - &.grid { - border-radius: var(--theme-group-element-radius); - } -} -.tableViewer { - composes: theme-background-secondary theme-text-on-secondary from global; - position: relative; - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} -.tableContent { - display: flex; - flex: 1; - overflow: hidden; -} -.tableData { - gap: 8px; -} -.tableData, -.pane, -.paneContent { - position: relative; - display: flex; - flex: 1; - flex-direction: column; - overflow: hidden; -} -.pane { - &:first-child { - position: relative; - } -} -.tablePresentationBar { - margin-top: 36px; - &:first-child { - margin-right: 4px; - } - &:last-child { - margin-left: 4px; - } -} -.loader { - position: absolute; - width: 100%; - height: 100%; -} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.module.css new file mode 100644 index 0000000000..e90a7df12f --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.module.css @@ -0,0 +1,82 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.split { + &:not(.disabled) { + gap: 8px; + } +} + +.paneContent { + composes: theme-background-surface theme-text-on-surface from global; + + &.grid { + border-radius: var(--theme-group-element-radius); + } +} + +.captureView { + flex: 1; + display: flex; + overflow: auto; + position: relative; +} + +.tableViewer { + composes: theme-background-secondary theme-text-on-secondary from global; + position: relative; + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} +.tableContent { + display: flex; + flex: 1; + overflow: hidden; +} +.tableData { + gap: 8px; +} +.tableData, +.pane, +.paneContent { + position: relative; + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; +} +.paneContent.grid:focus-visible::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + outline-offset: -1px; + outline: var(--theme-primary) auto 1px; +} +.pane { + &:first-child { + position: relative; + } +} +.tablePresentationBar { + margin-top: 36px; + &:first-child { + margin-right: 4px; + } + &:last-child { + margin-left: 4px; + } +} +.loader { + position: absolute; + width: 100%; + height: 100%; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx index 572c2fc3ae..cbefdce39b 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -17,26 +17,32 @@ import { s, Split, TextPlaceholder, + useListKeyboardNavigation, + useMergeRefs, useObjectRef, useObservableRef, useS, useSplitUserState, + useTranslate, } from '@cloudbeaver/core-blocks'; -import type { IDataContext } from '@cloudbeaver/core-data-context'; import { useService } from '@cloudbeaver/core-di'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { CaptureView } from '@cloudbeaver/core-view'; -import { ResultSetConstraintAction } from '../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction'; -import { DataPresentationService, DataPresentationType } from '../DataPresentationService'; -import type { IDataTableActionsPrivate } from './IDataTableActions'; -import { TableError } from './TableError'; -import { TableFooter } from './TableFooter/TableFooter'; -import { TableGrid } from './TableGrid'; -import { TableHeader } from './TableHeader/TableHeader'; -import { TablePresentationBar } from './TablePresentationBar/TablePresentationBar'; -import { TableToolsPanel } from './TableToolsPanel'; -import style from './TableViewer.m.css'; -import { TableViewerStorageService } from './TableViewerStorageService'; +import { DatabaseDataConstraintAction } from '../DatabaseDataModel/Actions/DatabaseDataConstraintAction.js'; +import { type IDatabaseDataOptions } from '../DatabaseDataModel/IDatabaseDataOptions.js'; +import { DataPresentationService, DataPresentationType } from '../DataPresentationService.js'; +import { isResultSetDataModel } from '../ResultSet/isResultSetDataModel.js'; +import { DataPresentation } from './DataPresentation.js'; +import { DataViewerViewService } from './DataViewerViewService.js'; +import type { IDataTableActionsPrivate } from './IDataTableActions.js'; +import { TableError } from './TableError.js'; +import { TableFooter } from './TableFooter/TableFooter.js'; +import { TableHeader } from './TableHeader/TableHeader.js'; +import { TablePresentationBar } from './TablePresentationBar/TablePresentationBar.js'; +import { TableToolsPanel } from './TableToolsPanel.js'; +import style from './TableViewer.module.css'; +import { TableViewerStorageService } from './TableViewerStorageService.js'; export interface TableViewerProps { tableId: string; @@ -45,7 +51,6 @@ export interface TableViewerProps { valuePresentationId: string | null | undefined; /** Display data in simple mode, some features will be hidden or disabled */ simple?: boolean; - context?: IDataContext; className?: string; onPresentationChange: (id: string) => void; onValuePresentationChange: (id: string | null) => void; @@ -53,31 +58,32 @@ export interface TableViewerProps { export const TableViewer = observer( forwardRef(function TableViewer( - { - tableId, - resultIndex = 0, - presentationId, - valuePresentationId, - simple = false, - context, - className, - onPresentationChange, - onValuePresentationChange, - }, + { tableId, resultIndex = 0, presentationId, valuePresentationId, simple = false, className, onPresentationChange, onValuePresentationChange }, ref, ) { + const translate = useTranslate(); const styles = useS(style); + const dataViewerView = useService(DataViewerViewService); const dataPresentationService = useService(DataPresentationService); const tableViewerStorageService = useService(TableViewerStorageService); const dataModel = tableViewerStorageService.get(tableId); - const result = dataModel?.getResult(resultIndex); + const result = dataModel?.source.getResult(resultIndex); const loading = dataModel?.isLoading() ?? true; const dataFormat = result?.dataFormat || ResultDataFormat.Resultset; const splitState = useSplitUserState('table-viewer'); + const navRef = useListKeyboardNavigation( + '[data-presentation][tabindex]:not(:disabled), [data-presentation-tools][tabindex]:not(:disabled), [data-presentation-header][tabindex]:not(:disabled), [data-presentation-bar][tabindex]:not(:disabled)', + ); + const mergedRef = useMergeRefs(ref, navRef); const localActions = useObjectRef({ clearConstraints() { - const constraints = dataModel?.source.tryGetAction(resultIndex, ResultSetConstraintAction); + const unknownModel = dataModel as any; + if (!isResultSetDataModel(unknownModel)) { + return; + } + + const constraints = unknownModel?.source.tryGetAction(resultIndex, DatabaseDataConstraintAction); if (constraints) { constraints.deleteAll(); @@ -153,9 +159,7 @@ export const TableViewer = observer( ['setPresentation', 'setValuePresentation', 'switchValuePresentation', 'closeValuePresentation'], ); - const needRefresh = getComputed( - () => dataModel?.source.error === null && dataModel.source.results.length === 0 && dataModel.source.outdated && dataModel.source.isLoadable(), - ); + const needRefresh = getComputed(() => !dataModel?.isDisabled(resultIndex) && dataModel?.source.isOutdated() && dataModel.source.isLoadable()); useEffect(() => { if (needRefresh) { @@ -178,21 +182,22 @@ export const TableViewer = observer( // }, [dataFormat]); if (!dataModel) { - return ; + return {translate('plugin_data_viewer_no_available_presentation')}; } const presentation = dataPresentationService.getSupported(DataPresentationType.main, dataFormat, presentationId, dataModel, resultIndex); if (!presentation) { - return There are no available presentation for data format: {dataFormat}; + return {translate('plugin_data_viewer_no_available_presentation')}; } const valuePresentation = valuePresentationId ? dataPresentationService.getSupported(DataPresentationType.toolsPanel, dataFormat, valuePresentationId, dataModel, resultIndex) : null; + const isStatistics = result?.loadedFully && !result.data; const resultExist = dataModel.source.hasResult(resultIndex); - const overlay = dataModel.source.results.length > 0 && presentation.dataFormat === dataFormat; + const overlay = getComputed(() => dataModel.source.results.length > 0 && presentation.dataFormat === dataFormat); const valuePanelDisplayed = valuePresentation && (valuePresentation.dataFormat === undefined || valuePresentation.dataFormat === dataFormat) && @@ -201,84 +206,94 @@ export const TableViewer = observer( !simple; return ( -
-
- -
- - - -
- - - - - dataModel.source.cancel()} - /> -
-
- - - -
- {resultExist && ( - +
+
+ {!isStatistics && ( + + )} +
+ + + +
+ + - )} + + + dataModel.source.cancel()} + />
- -
-
+ + + + +
+ {resultExist && ( + + )} +
+
+
+ +
+ {!simple && !isStatistics && ( + + )}
- {!simple && ( - - )} +
- -
+ ); }), ); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerLoader.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerLoader.ts index 82492390ee..3584e0f137 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerLoader.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerLoader.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,6 +8,6 @@ import React from 'react'; export const TableViewerLoader = React.lazy(async () => { - const { TableViewer } = await import('./TableViewer'); + const { TableViewer } = await import('./TableViewer.js'); return { default: TableViewer }; }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerStorageService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerStorageService.ts index 66a773c7ac..6fdc9c65cc 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerStorageService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerStorageService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,22 +8,23 @@ import { computed, makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; -import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel'; -import type { IDatabaseDataResult } from '../DatabaseDataModel/IDatabaseDataResult'; +import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel.js'; +import { type IDatabaseDataSource } from '../DatabaseDataModel/IDatabaseDataSource.js'; +import type { IDataViewerTableStorage } from '../IDataViewerTableStorage.js'; export interface ITableViewerStorageChangeEventData { type: 'add' | 'remove'; - model: IDatabaseDataModel; + model: IDatabaseDataModel; } @injectable() -export class TableViewerStorageService { +export class TableViewerStorageService implements IDataViewerTableStorage { readonly onChange: ISyncExecutor; - private readonly tableModelMap: Map> = new Map(); + private readonly tableModelMap: Map> = new Map(); - get values(): Array> { + get values(): Array> { return Array.from(this.tableModelMap.values()); } @@ -40,11 +41,11 @@ export class TableViewerStorageService { return this.tableModelMap.has(tableId); } - get>(tableId: string): T | undefined { + get = IDatabaseDataModel>(tableId: string): T | undefined { return this.tableModelMap.get(tableId) as any; } - add(model: IDatabaseDataModel): IDatabaseDataModel { + add = IDatabaseDataSource>(model: IDatabaseDataModel): IDatabaseDataModel { if (this.tableModelMap.has(model.id)) { return model; } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelBootstrap.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelBootstrap.ts index 4d56bcbfa8..bb59222c6c 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelBootstrap.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,22 +9,25 @@ import React from 'react'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { ResultSetDataAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataAction'; -import { DataPresentationService, DataPresentationType } from '../../DataPresentationService'; -import { DataValuePanelService } from './DataValuePanelService'; +import { DatabaseDataResultAction } from '../../DatabaseDataModel/Actions/DatabaseDataResultAction.js'; +import { DataPresentationService, DataPresentationType } from '../../DataPresentationService.js'; +import { DataValuePanelService } from './DataValuePanelService.js'; export const ValuePanel = React.lazy(async () => { - const { ValuePanel } = await import('./ValuePanel'); + const { ValuePanel } = await import('./ValuePanel.js'); return { default: ValuePanel }; }); -@injectable() +@injectable(() => [DataPresentationService, DataValuePanelService]) export class DataValuePanelBootstrap extends Bootstrap { - constructor(private readonly dataPresentationService: DataPresentationService, private readonly dataValuePanelService: DataValuePanelService) { + constructor( + private readonly dataPresentationService: DataPresentationService, + private readonly dataValuePanelService: DataValuePanelService, + ) { super(); } - register(): void | Promise { + override register(): void | Promise { this.dataPresentationService.add({ id: 'value-text-presentation', type: DataPresentationType.toolsPanel, @@ -35,12 +38,10 @@ export class DataValuePanelBootstrap extends Bootstrap { return true; } - const data = model.source.tryGetAction(resultIndex, ResultSetDataAction); + const data = model.source.getActionImplementation(resultIndex, DatabaseDataResultAction); return data?.empty || this.dataValuePanelService.getDisplayed({ model, resultIndex, dataFormat }).length === 0; }, getPresentationComponent: () => ValuePanel, }); } - - load(): void {} } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelService.ts index 87ea12ad2c..86bbc2f771 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/DataValuePanelService.ts @@ -1,40 +1,39 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { ITabInfo, ITabInfoOptions, TabsContainer } from '@cloudbeaver/core-ui'; +import { type ITabInfo, type ITabInfoOptions, TabsContainer } from '@cloudbeaver/core-ui'; -import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; -import type { IDatabaseDataResult } from '../../DatabaseDataModel/IDatabaseDataResult'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; export interface IDataValuePanelOptions { dataFormat: ResultDataFormat[]; } -export interface IDataValuePanelProps { +export interface IDataValuePanelProps { dataFormat: ResultDataFormat | null; - model: IDatabaseDataModel; + model: IDatabaseDataModel; resultIndex: number; } @injectable() export class DataValuePanelService { - readonly tabs: TabsContainer, IDataValuePanelOptions>; + readonly tabs: TabsContainer; constructor() { this.tabs = new TabsContainer('Value Panel'); } - get(tabId: string): ITabInfo, IDataValuePanelOptions> | undefined { + get(tabId: string): ITabInfo | undefined { return this.tabs.getTabInfo(tabId); } - getDisplayed(props?: IDataValuePanelProps): Array, IDataValuePanelOptions>> { + getDisplayed(props?: IDataValuePanelProps): Array> { return this.tabs.tabInfoList.filter( info => (props?.dataFormat === undefined || props.dataFormat === null || info.options?.dataFormat.includes(props.dataFormat)) && @@ -42,7 +41,7 @@ export class DataValuePanelService { ); } - add(tabInfo: ITabInfoOptions, IDataValuePanelOptions>): void { + add(tabInfo: ITabInfoOptions): void { this.tabs.add(tabInfo); } } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx index 239b24f84e..7b634ad603 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx @@ -1,81 +1,88 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useRef, useState } from 'react'; -import styled, { css } from 'reshadow'; +import { s, SContext, type StyleRegistry, useS } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { BASE_TAB_STYLES, TabList, TabPanelList, TabsState, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui'; +import { TabList, TabPanelList, TabPanelStyles, TabsState, TabStyles } from '@cloudbeaver/core-ui'; +import { MetadataMap } from '@cloudbeaver/core-utils'; -import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; -import type { DataPresentationComponent } from '../../DataPresentationService'; -import { DataValuePanelService } from './DataValuePanelService'; +import { DatabaseDataResultAction } from '../../DatabaseDataModel/Actions/DatabaseDataResultAction.js'; +import { DatabaseMetadataAction } from '../../DatabaseDataModel/Actions/DatabaseMetadataAction.js'; +import { DatabaseSelectAction } from '../../DatabaseDataModel/Actions/DatabaseSelectAction.js'; +import type { DataPresentationComponent } from '../../DataPresentationService.js'; +import { DataValuePanelService } from './DataValuePanelService.js'; +import styles from './shared/ValuePanel.module.css'; +import ValuePanelEditorTabPanel from './shared/ValuePanelEditorTabPanel.module.css'; +import ValuePanelEditorTabs from './shared/ValuePanelEditorTabs.module.css'; +import ValuePanelTab from './shared/ValuePanelTab.module.css'; -const styles = css` - table-left-bar { - display: flex; - } - TabList { - composes: theme-border-color-background from global; - position: relative; +const tabListRegistry: StyleRegistry = [[TabStyles, { mode: 'append', styles: [ValuePanelTab] }]]; - &:before { - content: ''; - position: absolute; - bottom: 0; - width: 100%; - border-bottom: solid 2px; - border-color: inherit; - } - } - TabList tab-outer:only-child { - display: none; - } - TabPanel { - padding-top: 8px; - } - TabList, - TabPanel { - composes: theme-background-secondary theme-text-on-secondary from global; - } -`; +const tabPanelListRegistry: StyleRegistry = [ + [TabStyles, { mode: 'append', styles: [ValuePanelEditorTabs] }], + [TabPanelStyles, { mode: 'append', styles: [ValuePanelEditorTabPanel] }], +]; -export const ValuePanel: DataPresentationComponent = observer(function ValuePanel({ dataFormat, model, resultIndex }) { +export const ValuePanel: DataPresentationComponent = observer(function ValuePanel({ dataFormat, model, resultIndex }) { const service = useService(DataValuePanelService); - const [currentTabId, setCurrentTabId] = useState(''); - const lastTabId = useRef(''); + const selectAction = model.source.getActionImplementation(resultIndex, DatabaseSelectAction); + const dataResultAction = model.source.getActionImplementation(resultIndex, DatabaseDataResultAction); + const metadataAction = model.source.getAction(resultIndex, DatabaseMetadataAction); + const activeElements = selectAction?.getActiveElements(); + let elementKey: string | null = null; + const style = useS(styles); + + if (dataResultAction && activeElements && activeElements.length > 0) { + elementKey = dataResultAction.getIdentifier(activeElements[0]); + } + + const state = metadataAction.get(`value-panel-${elementKey}`, () => + observable( + { + currentTabId: '', + tabsState: new MetadataMap(), + setCurrentTabId(tabId: string) { + this.currentTabId = tabId; + }, + }, + { tabsState: false }, + {}, + ), + ); const displayed = service.getDisplayed({ dataFormat, model, resultIndex }); + let currentTabId = state.currentTabId; + + const hasCurrentTabCells = currentTabId && displayed.some(tab => tab.key === currentTabId); - if (displayed.length > 0) { - const firstTabId = displayed[0].key; - if (firstTabId !== lastTabId.current) { - setCurrentTabId(firstTabId); - lastTabId.current = firstTabId; - } + if (displayed.length > 0 && !hasCurrentTabCells) { + currentTabId = displayed[0]!.key; } - return styled( - BASE_TAB_STYLES, - styles, - UNDERLINE_TAB_STYLES, - )( + return ( setCurrentTabId(tab.tabId)} + onChange={tab => state.setCurrentTabId(tab.tabId)} > - - - , + + + + + + + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanel.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanel.module.css new file mode 100644 index 0000000000..47c092049a --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanel.module.css @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tabList { + composes: theme-background-secondary theme-border-color-background theme-text-on-secondary from global; + position: relative; + &:before { + content: ''; + position: absolute; + bottom: 0; + width: 100%; + border-bottom: solid 2px; + border-color: inherit; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanelEditorTabPanel.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanelEditorTabPanel.module.css new file mode 100644 index 0000000000..fc75ed9253 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanelEditorTabPanel.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tabPanel { + composes: theme-background-secondary theme-text-on-secondary from global; + padding-top: 8px; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanelEditorTabs.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanelEditorTabs.module.css new file mode 100644 index 0000000000..0afe075a93 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanelEditorTabs.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tab { + border: none; +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanelTab.module.css b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanelTab.module.css new file mode 100644 index 0000000000..97ac1eb658 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/shared/ValuePanelTab.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tabOuter:only-child { + display: none; +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.module.css b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.module.css new file mode 100644 index 0000000000..1550c7ff0f --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.module.css @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.container { + display: flex; + flex-direction: column; + padding: 4px 10px; +} \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx index 6a4d9cdf8c..db79800135 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx @@ -1,82 +1,89 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled, { css } from 'reshadow'; import { Radio, TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks'; import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; +import { isDefined } from '@dbeaver/js-helpers'; -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 { isStringifiedBoolean } from './isBooleanValuePresentationAvailable'; +import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.js'; +import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; +import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.js'; +import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.js'; +import { isResultSetDataModel } from '../../ResultSet/isResultSetDataModel.js'; +import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js'; +import classes from './BooleanValuePresentation.module.css'; +import { preprocessBooleanValue } from './preprocessBooleanValue.js'; +import { DatabaseEditChangeType } from '../../DatabaseDataModel/Actions/IDatabaseDataEditAction.js'; -const styles = css` - container { - display: flex; - flex-direction: column; +export const BooleanValuePresentation: TabContainerPanelComponent = observer(function BooleanValuePresentation({ + model: unknownModel, + resultIndex, +}) { + const model = unknownModel as any; + if (!isResultSetDataModel(model)) { + throw new Error('BooleanValuePresentation can be used only with ResultSetDataSource'); } - Radio { - padding: 0; - } -`; - -export const BooleanValuePresentation: TabContainerPanelComponent> = observer( - function BooleanValuePresentation({ model, resultIndex }) { - const translate = useTranslate(); - const selection = model.source.getAction(resultIndex, ResultSetSelectAction); - const focusCell = selection.getFocusedElement(); - - if (!selection.elements.length && !focusCell) { - return null; - } - - let value: boolean | null | undefined; + const translate = useTranslate(); - const view = model.source.getAction(resultIndex, ResultSetViewAction); - const editor = model.source.getAction(resultIndex, ResultSetEditAction); + const selectAction = model.source.getAction(resultIndex, ResultSetSelectAction); + const viewAction = model.source.getAction(resultIndex, ResultSetViewAction); + const editAction = model.source.getAction(resultIndex, ResultSetEditAction); + const formatAction = model.source.getAction(resultIndex, ResultSetFormatAction); - const firstSelectedCell = selection.elements[0] || focusCell; - const cellValue = view.getCellValue(firstSelectedCell); + const activeElements = selectAction.getActiveElements(); - if (typeof cellValue === 'string' && isStringifiedBoolean(cellValue)) { - value = cellValue.toLowerCase() === 'true'; - } else if (typeof cellValue === 'boolean' || cellValue === null) { - value = cellValue; - } - - if (value === undefined) { - return {translate('data_viewer_presentation_value_boolean_placeholder')}; - } + if (activeElements.length === 0) { + return {translate('data_viewer_presentation_value_no_active_elements')}; + } - const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const firstSelectedCell = activeElements[0]!; + const cellValue = viewAction.getCellValue(firstSelectedCell); + const value = preprocessBooleanValue(cellValue); - const column = view.getColumn(firstSelectedCell.column); - const nullable = column?.required === false; - const readonly = model.isReadonly(resultIndex) || model.isDisabled(resultIndex) || format.isReadOnly(firstSelectedCell); + if (!isDefined(value)) { + return {translate('data_viewer_presentation_value_boolean_placeholder')}; + } - return styled(styles)( - - editor.set(firstSelectedCell, true)}> - TRUE - - editor.set(firstSelectedCell, false)}> - FALSE + const column = viewAction.getColumn(firstSelectedCell.column); + const nullable = column?.required === false; + const readonly = + model.isReadonly(resultIndex) || + model.isDisabled(resultIndex) || + (formatAction.isReadOnly(firstSelectedCell) && editAction.getElementState(firstSelectedCell) !== DatabaseEditChangeType.add); + return ( +
+ editAction.set(firstSelectedCell, true)} + > + TRUE + + editAction.set(firstSelectedCell, false)} + > + FALSE + + {nullable && ( + editAction.set(firstSelectedCell, null)} + > + NULL - {nullable && ( - editor.set(firstSelectedCell, null)}> - NULL - - )} - , - ); - }, -); + )} +
+ ); +}); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap.ts index 5901e98157..074b669f12 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap.ts @@ -1,60 +1,58 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; -import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction'; -import { DataValuePanelService } from '../../TableViewer/ValuePanel/DataValuePanelService'; -import { BooleanValuePresentation } from './BooleanValuePresentation'; -import { isBooleanValuePresentationAvailable } from './isBooleanValuePresentationAvailable'; +import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.js'; +import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.js'; +import { isResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; +import { DataValuePanelService } from '../../TableViewer/ValuePanel/DataValuePanelService.js'; +import { isBooleanValuePresentationAvailable } from './isBooleanValuePresentationAvailable.js'; -@injectable() +const BooleanValuePresentation = importLazyComponent(() => import('./BooleanValuePresentation.js').then(module => module.BooleanValuePresentation)); + +@injectable(() => [DataValuePanelService]) export class BooleanValuePresentationBootstrap extends Bootstrap { constructor(private readonly dataValuePanelService: DataValuePanelService) { super(); } - register(): void { + override register(): void { this.dataValuePanelService.add({ key: 'boolean-presentation', - options: { dataFormat: [ResultDataFormat.Resultset] }, + options: { + dataFormat: [ResultDataFormat.Resultset], + }, name: 'boolean', order: 1, panel: () => BooleanValuePresentation, isHidden: (_, context) => { - if (!context || !context.model.source.hasResult(context.resultIndex)) { + const source = context?.model.source as any; + if (!context || !isResultSetDataSource(source) || !source?.hasResult(context.resultIndex)) { return true; } - const selection = context.model.source.getAction(context.resultIndex, ResultSetSelectAction); - - const focusedElement = selection.getFocusedElement(); + const selection = source.getAction(context.resultIndex, ResultSetSelectAction); - if (selection.elements.length > 0 || focusedElement) { - const view = context.model.source.getAction(context.resultIndex, ResultSetViewAction); + const activeElements = selection.getActiveElements(); - const firstSelectedCell = selection.elements[0] || focusedElement; + if (activeElements.length > 0) { + const view = source.getAction(context.resultIndex, ResultSetViewAction); + const firstSelectedCell = activeElements[0]!; const cellValue = view.getCellValue(firstSelectedCell); - - if (cellValue === undefined) { - return true; - } - const column = view.getColumn(firstSelectedCell.column); - return column === undefined || !isBooleanValuePresentationAvailable(cellValue, column); + return cellValue === undefined || column === undefined || !isBooleanValuePresentationAvailable(cellValue, column); } return true; }, }); } - - load(): void {} } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/isBooleanValuePresentationAvailable.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/isBooleanValuePresentationAvailable.ts index c54ff41941..a8ff3dddf1 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/isBooleanValuePresentationAvailable.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/isBooleanValuePresentationAvailable.ts @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { SqlResultColumn } from '@cloudbeaver/core-sdk'; -import type { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; +import type { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; export function isStringifiedBoolean(value: string): boolean { return ['false', 'true'].includes(value.toLowerCase()); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/preprocessBooleanValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/preprocessBooleanValue.ts new file mode 100644 index 0000000000..48f72128f2 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/preprocessBooleanValue.ts @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; +import { isStringifiedBoolean } from './isBooleanValuePresentationAvailable.js'; + +export function preprocessBooleanValue(cellValue: IResultSetValue): boolean | null | undefined { + if (typeof cellValue === 'string' && isStringifiedBoolean(cellValue)) { + return cellValue.toLowerCase() === 'true'; + } + + if (typeof cellValue === 'boolean' || cellValue === null) { + return cellValue; + } + + return undefined; +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.module.css b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.module.css new file mode 100644 index 0000000000..ce19e39674 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.module.css @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.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..c1c4e81cf8 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx @@ -1,215 +1,130 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, computed, observable } from 'mobx'; +import { action, 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 { 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 { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; -import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; -import { ResultSetDataKeysUtils } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataKeysUtils'; -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; - } +import { useMemo } from 'react'; + +import { ActionIconButton, Button, Container, Fill, Loader, s, useS, useSuspense, useTranslate } from '@cloudbeaver/core-blocks'; +import { type TabContainerPanelComponent, useTabLocalState } from '@cloudbeaver/core-ui'; +import { blobToBase64, bytesToSize, throttle } from '@cloudbeaver/core-utils'; +import { isResultSetContentValue } from '@dbeaver/result-set-api'; + +import { isResultSetDataModel } from '../../ResultSet/isResultSetDataModel.js'; +import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js'; +import { QuotaPlaceholder } from '../QuotaPlaceholder.js'; +import styles from './ImageValuePresentation.module.css'; +import { useValuePanelImageValue } from './useValuePanelImageValue.js'; + +export const ImageValuePresentation: TabContainerPanelComponent = observer(function ImageValuePresentation({ + model: unknownModel, + resultIndex, +}) { + const model = unknownModel as any; + if (!isResultSetDataModel(model)) { + throw new Error('ImageValuePresentation can be used only with ResultSetDataSource'); } - - container { - display: flex; - gap: 16px; - flex: 1; - flex-direction: column; - } - - image { - flex: 1; - display: flex; - overflow: auto; - } -`; - -interface IToolsProps { - loading?: boolean; - stretch?: boolean; - onToggleStretch?: () => void; - onSave?: () => void; -} - -const Tools = observer(function Tools({ loading, stretch, onToggleStretch, onSave }) { const translate = useTranslate(); - - return styled(VALUE_PANEL_TOOLS_STYLES)( - - {onSave && ( - - )} - {onToggleStretch && ( - - - - - - - - - )} - , - ); -}); - -export const ImageValuePresentation: TabContainerPanelComponent> = observer( - function ImageValuePresentation({ model, resultIndex }) { - const translate = useTranslate(); - const notificationService = useService(NotificationService); - const quotasService = useService(QuotasService); - const style = useStyles(styles); - - const content = model.source.getAction(resultIndex, ResultSetDataContentAction); - - const state = useObservableRef( - () => ({ - get selectedCell() { - const selection = this.model.source.getAction(this.resultIndex, ResultSetSelectAction); - const focusCell = selection.getFocusedElement(); - - return selection.elements[0] || focusCell; - }, - get cellValue() { - const view = this.model.source.getAction(this.resultIndex, ResultSetViewAction); - const cellValue = view.getCellValue(this.selectedCell); - - return cellValue; - }, - get src() { - if (this.savedSrc) { - return this.savedSrc; - } - - 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)) { - return this.cellValue; - } - - return ''; - }, - get savedSrc() { - return content.retrieveFileDataUrlFromCache(this.selectedCell); - }, - get canSave() { - if (this.truncated) { - return content.isDownloadable(this.selectedCell); - } - - return !!this.src; - }, - get truncated() { - return isResultSetContentValue(this.cellValue) && content.isContentTruncated(this.cellValue); - }, + const suspense = useSuspense(); + const style = useS(styles); + const state = useTabLocalState(() => + observable( + { stretch: false, toggleStretch() { this.stretch = !this.stretch; }, - async save() { - try { - if (this.truncated) { - await content.downloadFileData(this.selectedCell); - } else { - download(this.src, '', true); - } - } catch (exception: any) { - this.notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error'); - } - }, - }), + }, { - selectedCell: computed, - cellValue: computed, - src: computed, - savedSrc: computed, - canSave: computed, - truncated: computed, stretch: observable.ref, - model: observable.ref, - resultIndex: observable.ref, toggleStretch: action.bound, - save: action.bound, }, - { model, resultIndex, notificationService }, - ); + ), + ); + const data = useValuePanelImageValue({ model, resultIndex }); + const loading = model.isLoading(); + const valueSize = bytesToSize(isResultSetContentValue(data.cellValue) ? (data.cellValue.contentLength ?? 0) : 0); + const isTruncatedMessageDisplay = !!data.truncated && !data.src; + const isDownloadable = isTruncatedMessageDisplay && !!data.selectedCell && data.contentAction.isDownloadable(data.selectedCell); + const isCacheDownloading = isDownloadable && data.contentAction.isLoading(data.selectedCell); + const debouncedDownload = useMemo(() => throttle(() => data.download(), 1000, false), []); + const srcGetter = suspense.observedValue( + 'src', + () => data.src, + async src => { + if (src instanceof Blob) { + return await blobToBase64(src); + } + return src; + }, + ); - const save = state.canSave ? state.save : undefined; - const loading = model.isLoading(); + function imageContextMenuHandler(event: React.MouseEvent) { + if (!data.canSave) { + event.preventDefault(); + } + } + + return ( + + + + {data.src && ( + + )} + {isTruncatedMessageDisplay && ( + + {isDownloadable && ( + + )} + + )} + + + + + {data.canSave && ( + + )} + {data.canUpload && ( + + )} + + + + + + + + ); +}); - if (state.truncated && !state.savedSrc) { - const limit = bytesToSize(quotasService.getQuota('sqlBinaryPreviewMaxLength')); - const valueSize = bytesToSize((state.cellValue as unknown as IResultSetContentValue).contentLength ?? 0); +interface ImageRendererProps { + className?: string; + srcGetter: () => string | null; + onContextMenu?: (event: React.MouseEvent) => void; +} - const load = async () => { - try { - await content.resolveFileDataUrl(state.selectedCell); - } catch (exception: any) { - notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error'); - } - }; +export const ImageRenderer = observer(function ImageRenderer({ srcGetter, className, onContextMenu }) { + const src = srcGetter(); - return styled(style)( - - - {content.isDownloadable(state.selectedCell) && ( - - )} - - - , - ); - } + if (!src) { + return null; + } - 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..9588f14e4b 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts @@ -1,74 +1,59 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; 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 { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; -import type { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; -import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; -import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction'; -import { DataValuePanelService } from '../../TableViewer/ValuePanel/DataValuePanelService'; -import { ImageValuePresentation } from './ImageValuePresentation'; +import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.js'; +import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.js'; +import { isResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; +import { DataValuePanelService } from '../../TableViewer/ValuePanel/DataValuePanelService.js'; +import { isImageValuePresentationAvailable } from './isImageValuePresentationAvailable.js'; -@injectable() +const ImageValuePresentation = importLazyComponent(() => import('./ImageValuePresentation.js').then(module => module.ImageValuePresentation)); + +@injectable(() => [DataValuePanelService]) export class ImageValuePresentationBootstrap extends Bootstrap { constructor(private readonly dataValuePanelService: DataValuePanelService) { super(); } - register(): void | Promise { + override register(): void | Promise { this.dataValuePanelService.add({ key: 'image-presentation', - options: { dataFormat: [ResultDataFormat.Resultset] }, + options: { + dataFormat: [ResultDataFormat.Resultset], + }, name: 'data_viewer_presentation_value_image_title', order: 1, panel: () => ImageValuePresentation, isHidden: (_, context) => { - if (!context?.model.source.hasResult(context.resultIndex)) { + const source = context?.model.source as any; + if (!context?.model.source.hasResult(context.resultIndex) || !isResultSetDataSource(source)) { return true; } - const selection = context.model.source.getAction(context.resultIndex, ResultSetSelectAction); + const selection = source.getAction(context.resultIndex, ResultSetSelectAction); - const focusedElement = selection.getFocusedElement(); + const activeElements = selection.getActiveElements(); - if (selection.elements.length > 0 || focusedElement) { - const view = context.model.source.getAction(context.resultIndex, ResultSetViewAction); + if (activeElements.length > 0) { + const view = source.getAction(context.resultIndex, ResultSetViewAction); - const firstSelectedCell = selection.elements[0] || focusedElement; + const firstSelectedCell = activeElements[0]!; const cellValue = view.getCellValue(firstSelectedCell); - return !(this.isImageUrl(cellValue) || (isResultSetContentValue(cellValue) && this.isImage(cellValue))); + return !isImageValuePresentationAvailable(cellValue); } return true; }, }); } - - load(): void {} - - private isImage(value: IResultSetContentValue | null) { - if (value !== null && 'binary' in value) { - return getMIME(value.binary || '') !== null; - } - - return false; - } - - private isImageUrl(value: IResultSetValue | undefined) { - if (typeof value !== 'string') { - return false; - } - - return isValidUrl(value) && isImageFormat(value); - } } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/isImageValuePresentationAvailable.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/isImageValuePresentationAvailable.ts new file mode 100644 index 0000000000..df65b899e1 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/isImageValuePresentationAvailable.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils'; + +import { isResultSetBinaryValue, isResultSetContentValue } from '@dbeaver/result-set-api'; +import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.js'; +import type { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; + +export function isImageValuePresentationAvailable(value: IResultSetValue) { + let contentType = null; + + if (isResultSetBinaryValue(value)) { + contentType = getMIME(value.binary); + } else if (isResultSetContentValue(value) || isResultSetBlobValue(value)) { + contentType = value?.contentType ?? null; + } + + if (contentType?.startsWith('image/')) { + return true; + } + + if (typeof value !== 'string') { + return false; + } + + return isValidUrl(value) && isImageFormat(value); +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts new file mode 100644 index 0000000000..77fcaa3099 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/useValuePanelImageValue.ts @@ -0,0 +1,169 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { action, computed, observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import { promptForFiles } from '@cloudbeaver/core-browser'; +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { download, getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils'; +import { isResultSetBinaryValue } from '@dbeaver/result-set-api'; + +import { createResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue.js'; +import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.js'; +import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.js'; +import { isResultSetFileValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetFileValue.js'; +import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.js'; +import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.js'; +import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; +import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.js'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import { DataViewerService } from '../../DataViewerService.js'; +import { ResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; + +interface Props { + model: IDatabaseDataModel; + resultIndex: number; +} + +export function useValuePanelImageValue({ model, resultIndex }: Props) { + const notificationService = useService(NotificationService); + const dataViewerService = useService(DataViewerService); + const selectAction = model.source.getAction(resultIndex, ResultSetSelectAction); + const formatAction = model.source.getAction(resultIndex, ResultSetFormatAction); + const contentAction = model.source.getAction(resultIndex, ResultSetDataContentAction); + const editAction = model.source.getAction(resultIndex, ResultSetEditAction); + + return useObservableRef( + () => ({ + get selectedCell(): IResultSetElementKey | undefined { + return this.selectAction.getActiveElements()?.[0]; + }, + get cellValue() { + if (this.selectedCell === undefined) { + return null; + } + + return this.formatAction.get(this.selectedCell); + }, + get src(): string | Blob | null { + if (isResultSetBlobValue(this.cellValue)) { + // uploaded file preview + return this.cellValue.blob; + } + + if (this.staticSrc) { + return this.staticSrc; + } + + if (this.cacheBlob) { + // uploaded file preview + return this.cacheBlob; + } + + return null; + }, + get staticSrc(): string | null { + if (this.truncated) { + return null; + } + + if (isResultSetBinaryValue(this.cellValue)) { + return `data:${getMIME(this.cellValue.binary)};base64,${this.cellValue.binary}`; + } + + if (typeof this.cellValue === 'string' && isValidUrl(this.cellValue) && isImageFormat(this.cellValue)) { + return this.cellValue; + } + + return null; + }, + get cacheBlob() { + if (!this.selectedCell) { + return null; + } + return this.contentAction.retrieveBlobFromCache(this.selectedCell); + }, + get canSave() { + if (!this.dataViewerService.canExportData) { + return false; + } + + if (this.truncated && this.selectedCell) { + return this.contentAction.isDownloadable(this.selectedCell); + } + + return this.staticSrc && !this.truncated; + }, + get canUpload() { + if (!this.selectedCell) { + return false; + } + return this.formatAction.isBinary(this.selectedCell); + }, + get truncated() { + if (isResultSetFileValue(this.cellValue)) { + return false; + } + + return this.selectedCell && this.contentAction.isBlobTruncated(this.selectedCell); + }, + async download() { + try { + if (this.src) { + download(this.src, '', true); + return; + } + + if (this.selectedCell) { + await this.contentAction.downloadFileData(this.selectedCell); + return; + } + + throw new Error("Can't save image"); + } catch (exception: any) { + this.notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error'); + } + }, + upload() { + promptForFiles().then(files => { + const file = files?.[0]; + if (file && this.selectedCell) { + this.editAction.set(this.selectedCell, createResultSetBlobValue(file)); + } + }); + }, + async loadFullImage() { + if (!this.selectedCell) { + return; + } + + try { + await this.contentAction.resolveFileDataUrl(this.selectedCell); + } catch (exception: any) { + this.notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error'); + } + }, + }), + { + selectedCell: computed, + cellValue: computed, + canUpload: computed, + src: computed, + cacheBlob: computed, + canSave: computed, + truncated: computed, + model: observable.ref, + resultIndex: observable.ref, + download: action.bound, + upload: action.bound, + loadFullImage: action.bound, + }, + { model, resultIndex, notificationService, selectAction, formatAction, contentAction, editAction, dataViewerService }, + ); +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/QuotaPlaceholder.module.css b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/QuotaPlaceholder.module.css new file mode 100644 index 0000000000..ba0ba863e7 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/QuotaPlaceholder.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.limitWord { + text-transform: lowercase; + display: contents; +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/QuotaPlaceholder.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/QuotaPlaceholder.tsx index b8349351bd..b77ed9fcaa 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/QuotaPlaceholder.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/QuotaPlaceholder.tsx @@ -1,69 +1,66 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled, { css } from 'reshadow'; -import { EAdminPermission } from '@cloudbeaver/core-authentication'; -import { Link, usePermission, useTranslate } from '@cloudbeaver/core-blocks'; +import { Container, Link, s, usePermission, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { WEBSITE_LINKS } from '@cloudbeaver/core-links'; +import { EAdminPermission } from '@cloudbeaver/core-root'; + +import type { IResultSetElementKey } from '../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.js'; +import { ResultSetDataContentAction } from '../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.js'; +import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel.js'; +import { ResultSetDataSource } from '../ResultSet/ResultSetDataSource.js'; +import styles from './QuotaPlaceholder.module.css'; interface Props { - limit?: string; - size?: string; className?: string; + elementKey: IResultSetElementKey | undefined; + model: IDatabaseDataModel; + resultIndex: number; + keepSize?: boolean; } -const style = css` - container { - margin: auto; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - } - p { - composes: theme-typography--body2 from global; - text-align: center; - margin: 0; - } - reason { - display: flex; - white-space: pre; - } - limit-word { - text-transform: lowercase; - } -`; - -export const QuotaPlaceholder: React.FC> = observer(function QuotaPlaceholder({ limit, size, className, children }) { +export const QuotaPlaceholder: React.FC> = observer(function QuotaPlaceholder({ + className, + children, + keepSize = false, + elementKey, + model, + resultIndex, +}) { const translate = useTranslate(); const admin = usePermission(EAdminPermission.admin); + const style = useS(styles); + const contentAction = model.source.getAction(resultIndex, ResultSetDataContentAction); + const limitInfo = elementKey ? contentAction.getLimitInfo(elementKey) : null; - return styled(style)( - -

- {translate('data_viewer_presentation_value_content_was_truncated')} - - {translate('data_viewer_presentation_value_content_truncated_placeholder') + ' '} - - {admin ? ( - - {translate('ui_limit')} - - ) : ( - translate('ui_limit') - )} - - - {limit && `${translate('ui_limit')}: ${limit}`} -
- {size && `${translate('data_viewer_presentation_value_content_value_size')}: ${size}`} -

- {children} -
, + return ( + +
+ {translate('data_viewer_presentation_value_content_truncated_placeholder')} +   + + {admin ? ( + + {translate('ui_limit')} + + ) : ( + translate('ui_limit') + )} + +
+ {children} +
); }); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/MAX_BLOB_PREVIEW_SIZE.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/MAX_BLOB_PREVIEW_SIZE.ts new file mode 100644 index 0000000000..9595f830e8 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/MAX_BLOB_PREVIEW_SIZE.ts @@ -0,0 +1,9 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export const MAX_BLOB_PREVIEW_SIZE = 10 * 1024; diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueEditor.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueEditor.tsx new file mode 100644 index 0000000000..6c26c9de62 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueEditor.tsx @@ -0,0 +1,41 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import { useMemo } from 'react'; + +import { EditorLoader, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; + +import { useDataViewerCopyHandler } from '../../useDataViewerCopyHandler.js'; +import { getTypeExtension } from './getTypeExtension.js'; + +interface Props { + contentType: string; + readonly: boolean; + lineWrapping: boolean; + valueGetter: () => string; + onChange: (value: string) => void; +} + +export const TextValueEditor = observer(function TextValueEditor({ contentType, valueGetter, readonly, lineWrapping, onChange }) { + const value = valueGetter(); + const typeExtension = useMemo(() => getTypeExtension(contentType!) ?? [], [contentType]); + + const extensions = useCodemirrorExtensions(undefined, typeExtension); + const copyEventHandler = useDataViewerCopyHandler(); + + return ( + + ); +}); 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..0cf646043b 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -1,207 +1,167 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useMemo } from 'react'; -import styled, { css } from 'reshadow'; -import { Button, useObservableRef, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { ActionIconButton, Container, Group, Loader, s, SContext, type StyleRegistry, useS, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { QuotasService } from '@cloudbeaver/core-root'; -import { BASE_TAB_STYLES, TabContainerPanelComponent, TabList, TabsState, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui'; -import { bytesToSize } from '@cloudbeaver/core-utils'; -import { EditorLoader, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; - -import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; -import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; -import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; -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'; -import { getTypeExtension } from './getTypeExtension'; -import { TextValuePresentationService } from './TextValuePresentationService'; -import { useAutoFormat } from './useAutoFormat'; - -const styles = css` - Tab { - composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global; - } - container { - display: flex; - gap: 8px; - flex-direction: column; - overflow: auto; - flex: 1; - } - actions { - display: flex; - justify-content: center; - flex: 0; - } - EditorLoader { - border-radius: var(--theme-group-element-radius); +import { type TabContainerPanelComponent, TabList, TabsState, TabStyles, useTabLocalState } from '@cloudbeaver/core-ui'; + +import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.js'; +import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.js'; +import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; +import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.js'; +import { DataViewerService } from '../../DataViewerService.js'; +import { isResultSetDataModel } from '../../ResultSet/isResultSetDataModel.js'; +import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js'; +import { getDefaultLineWrapping } from './getDefaultLineWrapping.js'; +import { isTextValueReadonly } from './isTextValueReadonly.js'; +import styles from './shared/TextValuePresentation.module.css'; +import TextValuePresentationTab from './shared/TextValuePresentationTab.module.css'; +import { TextValueEditor } from './TextValueEditor.js'; +import { TextValuePresentationService } from './TextValuePresentationService.js'; +import { TextValueTruncatedMessage } from './TextValueTruncatedMessage.js'; +import { useAutoContentType } from './useAutoContentType.js'; +import { useTextValueGetter } from './useTextValueGetter.js'; + +const tabRegistry: StyleRegistry = [[TabStyles, { mode: 'append', styles: [TextValuePresentationTab] }]]; + +export const TextValuePresentation: TabContainerPanelComponent = observer(function TextValuePresentation({ + model: unknownModel, + resultIndex, + dataFormat, +}) { + const model = unknownModel as any; + if (!isResultSetDataModel(model)) { + throw new Error('TextValuePresentation can be used only with ResultSetDataSource'); } - EditorLoader { - flex: 1; - overflow: auto; - } - TabList { - composes: theme-border-color-background theme-background-background from global; - overflow: auto; - border-radius: var(--theme-group-element-radius); - - & Tab { - border-bottom: 0; - - &:global([aria-selected='false']) { - border-bottom: 0 !important; - } - } - } -`; - -export const TextValuePresentation: TabContainerPanelComponent> = observer( - function TextValuePresentation({ model, resultIndex }) { - const translate = useTranslate(); - const notificationService = useService(NotificationService); - const quotasService = useService(QuotasService); - const textValuePresentationService = useService(TextValuePresentationService); - const style = useStyles(styles, UNDERLINE_TAB_STYLES, VALUE_PANEL_TOOLS_STYLES); - - const state = useObservableRef( - () => ({ - currentContentType: 'text/plain', - lastContentType: 'text/plain', - - setContentType(type: string) { - this.currentContentType = type; - }, - setDefaultContentType(type: string) { - this.currentContentType = type; - this.lastContentType = type; - }, - }), - { - currentContentType: observable.ref, - lastContentType: observable.ref, + const translate = useTranslate(); + const notificationService = useService(NotificationService); + const textValuePresentationService = useService(TextValuePresentationService); + const dataViewerService = useService(DataViewerService); + const style = useS(styles, TextValuePresentationTab); + const selectAction = model.source.getAction(resultIndex, ResultSetSelectAction); + const formatAction = model.source.getAction(resultIndex, ResultSetFormatAction); + const activeElements = selectAction.getActiveElements(); + const firstSelectedCell = activeElements.length ? activeElements[0] : undefined; + const contentAction = model.source.getAction(resultIndex, ResultSetDataContentAction); + const editAction = model.source.getAction(resultIndex, ResultSetEditAction); + + const state = useTabLocalState(() => + observable({ + lineWrapping: null as boolean | null, + currentContentType: null as string | null, + + setContentType(contentType: string | null) { + this.currentContentType = contentType; }, - false, - ['setContentType', 'setDefaultContentType'], - ); - - const selection = model.source.getAction(resultIndex, ResultSetSelectAction); - const editor = model.source.getAction(resultIndex, ResultSetEditAction); - const content = model.source.getAction(resultIndex, ResultSetDataContentAction); - - const focusCell = selection.getFocusedElement(); - - let stringValue = ''; - let contentType = 'text/plain'; - let firstSelectedCell: IResultSetElementKey | undefined; - let readonly = true; - let valueTruncated = false; - let limit: string | undefined; - let valueSize: string | undefined; - - if (selection.elements.length > 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); - - if (isResultSetContentValue(value)) { - valueTruncated = content.isContentTruncated(value); - - if (valueTruncated) { - limit = bytesToSize(quotasService.getQuota('sqlBinaryPreviewMaxLength')); - valueSize = bytesToSize(value.contentLength ?? 0); - } - - if (value.contentType) { - contentType = value.contentType; - - if (contentType === 'text/json') { - contentType = 'application/json'; - } - - if (!textValuePresentationService.tabs.has(contentType)) { - contentType = 'text/plain'; - } - } - } + setLineWrapping(lineWrapping: boolean | null) { + this.lineWrapping = lineWrapping; + }, + }), + ); + const contentType = useAutoContentType({ + dataFormat, + model, + resultIndex, + currentContentType: state.currentContentType, + elementKey: firstSelectedCell, + formatAction, + }); + const textValueGetter = useTextValueGetter({ + contentAction, + editAction, + formatAction, + dataFormat, + contentType, + elementKey: firstSelectedCell, + }); + const autoLineWrapping = getDefaultLineWrapping(contentType); + const lineWrapping = state.lineWrapping ?? autoLineWrapping; + const isReadonly = isTextValueReadonly({ model, resultIndex, contentAction, cell: firstSelectedCell, formatAction, editAction }); + const canSave = firstSelectedCell && contentAction.isDownloadable(firstSelectedCell) && dataViewerService.canExportData; + + function valueChangeHandler(newValue: string) { + if (firstSelectedCell && !isReadonly) { + editAction.set(firstSelectedCell, newValue); } + } - readonly = model.isReadonly(resultIndex) || model.isDisabled(resultIndex) || readonly; - - if (contentType !== state.lastContentType) { - state.setDefaultContentType(contentType); + async function saveHandler() { + if (!firstSelectedCell) { + return; } - const formatter = useAutoFormat(); - - function handleChange(newValue: string) { - if (firstSelectedCell && !readonly) { - editor.set(firstSelectedCell, newValue); - } + try { + await contentAction.downloadFileData(firstSelectedCell); + } catch (exception) { + notificationService.logException(exception as any, 'data_viewer_presentation_value_content_download_error'); } + } - async function save() { - if (!firstSelectedCell) { - return; - } - - try { - await content.downloadFileData(firstSelectedCell); - } catch (exception) { - notificationService.logException(exception as any, 'data_viewer_presentation_value_content_download_error'); - } + async function selectTabHandler(tabId: string) { + // currentContentType may be selected automatically we don't want to change state in this case + if (tabId !== contentType) { + state.setContentType(tabId); } + } - const autoFormat = !!firstSelectedCell && !editor.isElementEdited(firstSelectedCell); - const canSave = !!firstSelectedCell && content.isDownloadable(firstSelectedCell); - const typeExtension = useMemo(() => getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]); - const extensions = useCodemirrorExtensions(undefined, typeExtension); - - const value = autoFormat ? formatter.format(state.currentContentType, stringValue) : stringValue; + function toggleLineWrappingHandler() { + state.setLineWrapping(!lineWrapping); + } - return styled(style)( - - + return ( + + + state.setContentType(tab.tabId)} + onChange={tab => selectTabHandler(tab.tabId)} > - + + + - - handleChange(value)} /> - {valueTruncated && } - {canSave && ( - - - - )} - , - ); - }, -); +
+ + + + + + + {firstSelectedCell && } + + + {canSave && ( + + )} + + + + + ); +}); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts index a3faaf8720..e9ae3855aa 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts @@ -1,24 +1,24 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import React, { lazy } from 'react'; +import React from 'react'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { DataValuePanelService } from '../../TableViewer/ValuePanel/DataValuePanelService'; -import { TextValuePresentationService } from './TextValuePresentationService'; +import { isResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; +import { DataValuePanelService } from '../../TableViewer/ValuePanel/DataValuePanelService.js'; +import { isBlobPresentationAvailable } from './isTextValuePresentationAvailable.js'; +import { TextValuePresentationService } from './TextValuePresentationService.js'; -const TextValuePresentation = lazy(async () => { - const { TextValuePresentation } = await import('./TextValuePresentation'); - return { default: TextValuePresentation }; -}); +const TextValuePresentation = importLazyComponent(() => import('./TextValuePresentation.js').then(module => module.TextValuePresentation)); -@injectable() +@injectable(() => [TextValuePresentationService, DataValuePanelService]) export class TextValuePresentationBootstrap extends Bootstrap { constructor( private readonly textValuePresentationService: TextValuePresentationService, @@ -27,13 +27,18 @@ export class TextValuePresentationBootstrap extends Bootstrap { super(); } - register(): void | Promise { + override register(): void { this.dataValuePanelService.add({ key: 'text-presentation', - options: { dataFormat: [ResultDataFormat.Resultset] }, + options: { + dataFormat: [ResultDataFormat.Resultset], + }, name: 'data_viewer_presentation_value_text_title', order: Number.MAX_SAFE_INTEGER, panel: () => TextValuePresentation, + isHidden(_, props) { + return !props || !props.model.source.hasResult(props.resultIndex) || !isResultSetDataSource(props.model.source); + }, }); this.textValuePresentationService.add({ @@ -47,20 +52,36 @@ export class TextValuePresentationBootstrap extends Bootstrap { name: 'data_viewer_presentation_value_text_html_title', order: Number.MAX_SAFE_INTEGER, panel: () => React.Fragment, + // isHidden: (_, context) => isBlobPresentationAvailable(context), }); this.textValuePresentationService.add({ key: 'text/xml', name: 'data_viewer_presentation_value_text_xml_title', order: Number.MAX_SAFE_INTEGER, panel: () => React.Fragment, + // isHidden: (_, context) => isBlobPresentationAvailable(context), }); this.textValuePresentationService.add({ key: 'application/json', name: 'data_viewer_presentation_value_text_json_title', order: Number.MAX_SAFE_INTEGER, panel: () => React.Fragment, + // isHidden: (_, context) => isBlobPresentationAvailable(context), }); - } - load(): void {} + this.textValuePresentationService.add({ + key: 'application/octet-stream;type=hex', + name: 'data_viewer_presentation_value_text_hex_title', + order: Number.MAX_SAFE_INTEGER, + panel: () => React.Fragment, + isHidden: (_, context) => !isBlobPresentationAvailable(context), + }); + this.textValuePresentationService.add({ + key: 'application/octet-stream;type=base64', + name: 'data_viewer_presentation_value_text_base64_title', + order: Number.MAX_SAFE_INTEGER, + panel: () => React.Fragment, + isHidden: (_, context) => !isBlobPresentationAvailable(context), + }); + } } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts index 5d08269de2..4a583185fc 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts @@ -1,26 +1,34 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; -import { ITabInfo, ITabInfoOptions, TabsContainer } from '@cloudbeaver/core-ui'; +import { type ITabInfo, type ITabInfoOptions, TabsContainer } from '@cloudbeaver/core-ui'; + +import { type IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import { ResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; +import type { IDataValuePanelOptions, IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js'; + +export interface ITextValuePanelProps extends Omit { + model: IDatabaseDataModel; +} @injectable() export class TextValuePresentationService { - readonly tabs: TabsContainer; + readonly tabs: TabsContainer; constructor() { this.tabs = new TabsContainer('Value presentation'); } - get(tabId: string): ITabInfo | undefined { + get(tabId: string): ITabInfo | undefined { return this.tabs.getTabInfo(tabId); } - add(tabInfo: ITabInfoOptions): void { + add(tabInfo: ITabInfoOptions): void { this.tabs.add(tabInfo); } } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueTruncatedMessage.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueTruncatedMessage.tsx new file mode 100644 index 0000000000..b04e8a1d0e --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValueTruncatedMessage.tsx @@ -0,0 +1,73 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Button, Container, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { isResultSetContentValue } from '@dbeaver/result-set-api'; +import { bytesToSize } from '@cloudbeaver/core-utils'; +import { isNotNullDefined } from '@dbeaver/js-helpers'; + +import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.js'; +import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.js'; +import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.js'; +import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import { ResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; +import { QuotaPlaceholder } from '../QuotaPlaceholder.js'; +import { MAX_BLOB_PREVIEW_SIZE } from './MAX_BLOB_PREVIEW_SIZE.js'; + +interface Props { + resultIndex: number; + model: IDatabaseDataModel; + elementKey: IResultSetElementKey; +} + +export const TextValueTruncatedMessage = observer(function TextValueTruncatedMessage({ model, resultIndex, elementKey }) { + const translate = useTranslate(); + const notificationService = useService(NotificationService); + const contentAction = model.source.getAction(resultIndex, ResultSetDataContentAction); + const formatAction = model.source.getAction(resultIndex, ResultSetFormatAction); + const contentValue = formatAction.get(elementKey); + let isTruncated = contentAction.isTextTruncated(elementKey); + const isCacheLoaded = !!contentAction.retrieveFullTextFromCache(elementKey); + const limitInfo = elementKey ? contentAction.getLimitInfo(elementKey) : null; + + if (isResultSetBlobValue(contentValue)) { + isTruncated ||= contentValue.blob.size > (limitInfo?.limit ?? MAX_BLOB_PREVIEW_SIZE); + } + + if (!isTruncated || isCacheLoaded) { + return null; + } + + const isTextColumn = formatAction.isText(elementKey); + const valueSize = + isResultSetContentValue(contentValue) && isNotNullDefined(contentValue.contentLength) ? bytesToSize(contentValue.contentLength) : undefined; + + async function pasteFullText() { + try { + await contentAction.getFileFullText(elementKey); + } catch (exception) { + notificationService.logException(exception as any, 'data_viewer_presentation_value_content_paste_error'); + } + } + + return ( + + {isTextColumn && ( + + + + )} + + ); +}); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/formatText.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/formatText.ts new file mode 100644 index 0000000000..ba75276ec3 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/formatText.ts @@ -0,0 +1,29 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { textToHex } from '@cloudbeaver/core-utils'; + +export function formatText(type: string, value: string) { + try { + switch (type) { + case 'application/json': + return JSON.stringify(JSON.parse(value), null, 2); + case 'text/xml': + case 'text/html': + return value; + case 'application/octet-stream;type=hex': + return textToHex(value); + case 'application/octet-stream;type=base64': + case 'application/octet-stream': + return btoa(value); + default: + return value; + } + } catch { + return value; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/getDefaultLineWrapping.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/getDefaultLineWrapping.ts new file mode 100644 index 0000000000..99b842c4f8 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/getDefaultLineWrapping.ts @@ -0,0 +1,24 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { parseMIME } from '@cloudbeaver/core-utils'; + +export function getDefaultLineWrapping(mime: string): boolean { + const parsed = parseMIME(mime); + + // let's just list supported mime types here + switch (parsed.essence) { + case 'application/json': + case 'text/plain': + case 'text/xml': + case 'text/html': + case 'application/octet-stream': + return true; + default: + return true; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/getTypeExtension.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/getTypeExtension.ts index 69fddfa9ab..1c5f0b5086 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/getTypeExtension.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/getTypeExtension.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValuePresentationAvailable.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValuePresentationAvailable.ts new file mode 100644 index 0000000000..43d4169337 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValuePresentationAvailable.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { isResultSetBinaryValue } from '@dbeaver/result-set-api'; +import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.js'; +import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.js'; +import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.js'; +import { type ITextValuePanelProps } from './TextValuePresentationService.js'; + +export function isBlobPresentationAvailable(context: ITextValuePanelProps | undefined): boolean { + const source = context?.model.source; + if (!context || !source?.hasResult(context.resultIndex)) { + return true; + } + + const selection = source.getAction(context.resultIndex, ResultSetSelectAction); + + const activeElements = selection.getActiveElements(); + + if (activeElements.length > 0) { + const view = source.getAction(context.resultIndex, ResultSetViewAction); + + const firstSelectedCell = activeElements[0]!; + + const cellValue = view.getCellValue(firstSelectedCell); + + return isResultSetBinaryValue(cellValue) || isResultSetBlobValue(cellValue); + } + + return false; +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValueReadonly.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValueReadonly.ts new file mode 100644 index 0000000000..f24789e820 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValueReadonly.ts @@ -0,0 +1,38 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { DatabaseEditChangeType } from '../../DatabaseDataModel/Actions/IDatabaseDataEditAction.js'; +import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.js'; +import type { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.js'; +import type { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.js'; +import type { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import { ResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; + +interface Args { + contentAction: ResultSetDataContentAction; + formatAction: ResultSetFormatAction; + model: IDatabaseDataModel; + resultIndex: number; + cell: IResultSetElementKey | undefined; + editAction: ResultSetEditAction; +} + +export function isTextValueReadonly({ contentAction, formatAction, model, resultIndex, cell, editAction }: Args) { + if (!cell) { + return true; + } + + return ( + model.isReadonly(resultIndex) || + model.isDisabled(resultIndex) || + (formatAction.isReadOnly(cell) && editAction.getElementState(cell) !== DatabaseEditChangeType.add) || + formatAction.isBinary(cell) || + formatAction.isGeometry(cell) || + contentAction.isTextTruncated(cell) + ); +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentation.module.css b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentation.module.css new file mode 100644 index 0000000000..a26001f695 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentation.module.css @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tabList { + composes: theme-border-color-background theme-background-background from global; + overflow: auto; + border-radius: var(--theme-group-element-radius); +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentationTab.module.css b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentationTab.module.css new file mode 100644 index 0000000000..e473724bad --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/shared/TextValuePresentationTab.module.css @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.tab { + composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global; +} + +.textValuePresentationTab.underline .tab { + border-bottom: 0; + + &:global([aria-selected='false']) { + border-bottom: 0 !important; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoContentType.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoContentType.ts new file mode 100644 index 0000000000..3a7af5c642 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoContentType.ts @@ -0,0 +1,77 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { useService } from '@cloudbeaver/core-di'; +import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { isResultSetContentValue } from '@dbeaver/result-set-api'; + +import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.js'; +import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.js'; +import type { IResultSetValue, ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js'; +import { ResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js'; +import { TextValuePresentationService } from './TextValuePresentationService.js'; + +interface Args { + resultIndex: number; + model: IDatabaseDataModel; + dataFormat: ResultDataFormat | null; + currentContentType: string | null; + elementKey?: IResultSetElementKey; + formatAction: ResultSetFormatAction; +} + +const DEFAULT_CONTENT_TYPE = 'text/plain'; + +function getContentTypeFromResultSetValue(contentValue: IResultSetValue) { + if (isResultSetContentValue(contentValue)) { + return contentValue.contentType; + } + + if (isResultSetBlobValue(contentValue)) { + return contentValue.blob.type; + } + + return null; +} + +function preprocessDefaultContentType(contentType: string | null | undefined) { + if (contentType) { + switch (contentType) { + case 'text/json': + return 'application/json'; + case 'application/octet-stream': + return 'application/octet-stream;type=base64'; + default: + return contentType; + } + } + + return DEFAULT_CONTENT_TYPE; +} + +export function useAutoContentType({ dataFormat, model, formatAction, resultIndex, currentContentType, elementKey }: Args) { + const textValuePresentationService = useService(TextValuePresentationService); + const activeTabs = textValuePresentationService.tabs.getDisplayed({ + dataFormat: dataFormat, + model, + resultIndex: resultIndex, + }); + const contentValue = elementKey ? formatAction.get(elementKey) : null; + const contentValueType = getContentTypeFromResultSetValue(contentValue); + const defaultContentType = preprocessDefaultContentType(contentValueType); + + if (currentContentType === null) { + currentContentType = defaultContentType; + } + + if (activeTabs.length > 0 && !activeTabs.some(tab => tab.key === currentContentType)) { + currentContentType = activeTabs[0]!.key; + } + + return currentContentType; +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoFormat.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoFormat.ts deleted file mode 100644 index 80a7f8b711..0000000000 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoFormat.ts +++ /dev/null @@ -1,31 +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 { useObjectRef } from '@cloudbeaver/core-blocks'; - -export function useAutoFormat() { - return useObjectRef( - () => ({ - format(type: string, value: string) { - try { - switch (type) { - case 'application/json': - return JSON.stringify(JSON.parse(value), null, 2); - case 'text/xml': - case 'text/html': - return value; - default: - return value; - } - } catch { - return value; - } - }, - }), - false, - ); -} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValueGetter.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValueGetter.ts new file mode 100644 index 0000000000..d71bd9df1d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValueGetter.ts @@ -0,0 +1,98 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { observable } from 'mobx'; + +import { useObservableRef, useSuspense } from '@cloudbeaver/core-blocks'; +import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { isResultSetContentValue } from '@dbeaver/result-set-api'; +import { blobToBase64, removeMetadataFromDataURL } from '@cloudbeaver/core-utils'; +import { isNotNullDefined } from '@dbeaver/js-helpers'; + +import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.js'; +import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.js'; +import type { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.js'; +import type { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.js'; +import type { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; +import { formatText } from './formatText.js'; +import { MAX_BLOB_PREVIEW_SIZE } from './MAX_BLOB_PREVIEW_SIZE.js'; + +interface IUseTextValueArgs { + dataFormat: ResultDataFormat | null; + contentType: string; + elementKey?: IResultSetElementKey; + contentAction: ResultSetDataContentAction; + formatAction: ResultSetFormatAction; + editAction: ResultSetEditAction; +} + +type ValueGetter = () => string; + +export function useTextValueGetter({ contentType, elementKey, formatAction, contentAction, editAction }: IUseTextValueArgs): ValueGetter { + const suspense = useSuspense(); + const contentValue = elementKey ? formatAction.get(elementKey) : null; + const limitInfo = elementKey ? contentAction.getLimitInfo(elementKey) : null; + const observedContentValue = useObservableRef( + { + contentValue, + limitInfo, + }, + { contentValue: observable.ref, limitInfo: observable.ref }, + ); + + const parsedBlobValueGetter = suspense.observedValue( + 'value-blob', + () => ({ + blob: isResultSetBlobValue(observedContentValue.contentValue) ? observedContentValue.contentValue.blob : null, + limit: observedContentValue.limitInfo?.limit, + }), + async ({ blob, limit }) => { + if (!blob) { + return null; + } + const dataURL = await blobToBase64(blob, limit ?? MAX_BLOB_PREVIEW_SIZE); + + if (!dataURL) { + return null; + } + + return removeMetadataFromDataURL(dataURL); + }, + ); + + function valueGetter() { + let value = ''; + + if (!isNotNullDefined(elementKey)) { + return value; + } + + const contentValue = formatAction.get(elementKey); + const isBinary = formatAction.isBinary(elementKey); + const cachedFullText = contentAction.retrieveFullTextFromCache(elementKey); + + if (isBinary && isResultSetContentValue(contentValue)) { + if (contentValue.binary) { + value = atob(contentValue.binary); + } else if (contentValue.text) { + value = contentValue.text; + } + } else if (isResultSetBlobValue(contentValue)) { + value = atob(parsedBlobValueGetter() ?? ''); + } else { + value = cachedFullText || formatAction.getText(elementKey); + } + + if (!editAction.isElementEdited(elementKey) || isBinary) { + value = formatText(contentType, value); + } + + return value; + } + + return valueGetter; +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ValuePanelTools/VALUE_PANEL_TOOLS_STYLES.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ValuePanelTools/VALUE_PANEL_TOOLS_STYLES.ts deleted file mode 100644 index 1a446d8ec9..0000000000 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ValuePanelTools/VALUE_PANEL_TOOLS_STYLES.ts +++ /dev/null @@ -1,35 +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 VALUE_PANEL_TOOLS_STYLES = css` - tools-container { - display: flex; - justify-content: space-between; - gap: 8px; - } - tools { - composes: theme-background-surface theme-text-on-surface theme-border-color-background theme-form-element-radius from global; - display: flex; - box-sizing: border-box; - border: 2px solid; - } - tools-action { - composes: theme-ripple from global; - box-sizing: border-box; - background: inherit; - cursor: pointer; - padding: 4px; - width: 24px; - height: auto; - } - IconOrImage { - width: 100%; - height: 100%; - } -`; diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index cdc7eff691..db2a2a308f 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -1,61 +1,92 @@ -export * from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 * from './DatabaseDataModel/Actions/Document/DocumentDataAction'; -export * from './DatabaseDataModel/Actions/Document/DocumentEditAction'; -export * from './DatabaseDataModel/Actions/Document/IDatabaseDataDocument'; -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/IResultSetDataKey'; -export * from './DatabaseDataModel/Actions/ResultSet/IResultSetContentValue'; -export * from './DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; -export * from './DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction'; -export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataAction'; -export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataKeysUtils'; -export * from './DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; -export * from './DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; -export * from './DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; -export * from './DatabaseDataModel/Actions/ResultSet/ResultSetViewAction'; -export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; -export * from './DatabaseDataModel/Actions/DatabaseDataActionDecorator'; -export * from './DatabaseDataModel/Actions/DatabaseEditAction'; -export * from './DatabaseDataModel/Actions/DatabaseSelectAction'; -export * from './DatabaseDataModel/Actions/IDatabaseDataConstraintAction'; -export * from './DatabaseDataModel/Actions/IDatabaseDataEditAction'; -export * from './DatabaseDataModel/Actions/IDatabaseDataFormatAction'; -export * from './DatabaseDataModel/Actions/IDatabaseDataSelectAction'; -export * from './DatabaseDataModel/DatabaseDataAction'; -export * from './DatabaseDataModel/DatabaseDataActions'; -export * from './DatabaseDataModel/DatabaseDataFormat'; -export * from './DatabaseDataModel/DatabaseDataModel'; -export * from './DatabaseDataModel/DatabaseDataSource'; -export * from './DatabaseDataModel/IDatabaseDataAction'; -export * from './DatabaseDataModel/IDatabaseDataActions'; -export * from './DatabaseDataModel/IDatabaseDataEditor'; -export * from './DatabaseDataModel/IDatabaseDataModel'; -export * from './DatabaseDataModel/IDatabaseDataOptions'; -export * from './DatabaseDataModel/IDatabaseDataResult'; -export * from './DatabaseDataModel/IDatabaseDataSource'; -export * from './DatabaseDataModel/IDatabaseResultSet'; -export * from './DatabaseDataModel/Order'; -export * from './DataViewerService'; +import './module.js'; +export * from './manifest.js'; + +export * from './DatabaseDataModel/Actions/Document/DocumentDataAction.js'; +export * from './DatabaseDataModel/Actions/Document/DocumentEditAction.js'; +export * from './DatabaseDataModel/Actions/Document/IDatabaseDataDocument.js'; +export * from './DatabaseDataModel/Actions/Document/IDocumentElementKey.js'; +export * from './DatabaseDataModel/Actions/ResultSet/DataContext/DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY.js'; +export * from './DatabaseDataModel/DataContext/DATA_CONTEXT_DV_PRESENTATION.js'; +export * from './DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM.js'; +export * from './DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX.js'; +export * from './TableViewer/DATA_CONTEXT_DV_SIMPLE.js'; +export * from './DatabaseDataModel/Actions/ResultSet/DATA_CONTEXT_DV_RESULT_KEY.js'; +export * from './TableViewer/DATA_CONTEXT_DV_ACTIONS.js'; +export * from './TableViewer/DATA_CONTEXT_DV_PRESENTATION_ACTIONS.js'; +export * from './DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys.js'; +export * from './DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue.js'; +export * from './DatabaseDataModel/Actions/ResultSet/createResultSetContentValue.js'; +export * from './DatabaseDataModel/Actions/ResultSet/createResultSetFileValue.js'; +export * from './DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.js'; +export * from './DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue.js'; +export * from './DatabaseDataModel/Actions/ResultSet/IResultSetFileValue.js'; +export * from './DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue.js'; +export * from './DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.js'; +export * from './DatabaseDataModel/Actions/ResultSet/isResultSetFileValue.js'; +export * from './DatabaseDataModel/Actions/ResultSet/isResultSetGeometryValue.js'; +export * from './DatabaseDataModel/Actions/DatabaseDataConstraintAction.js'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.js'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataKeysUtils.js'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetEditAction.js'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.js'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.js'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.js'; +export * from './DatabaseDataModel/Actions/DatabaseDataActionDecorator.js'; +export * from './DatabaseDataModel/Actions/DatabaseDataResultAction.js'; +export * from './DatabaseDataModel/Actions/DatabaseEditAction.js'; +export * from './DatabaseDataModel/Actions/DatabaseMetadataAction.js'; +export * from './DatabaseDataModel/Actions/DatabaseSelectAction.js'; +export * from './DatabaseDataModel/Actions/IDatabaseDataConstraintAction.js'; +export * from './DatabaseDataModel/Actions/IDatabaseDataEditAction.js'; +export * from './DatabaseDataModel/Actions/IDatabaseDataFormatAction.js'; +export * from './DatabaseDataModel/Actions/IDatabaseDataMetadataAction.js'; +export * from './DatabaseDataModel/Actions/IDatabaseDataResultAction.js'; +export * from './DatabaseDataModel/Actions/IDatabaseDataSelectAction.js'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.js'; +export * from './DatabaseDataModel/DatabaseDataAction.js'; +export * from './DatabaseDataModel/DatabaseDataActions.js'; +export * from './DatabaseDataModel/DatabaseDataFormat.js'; +export * from './DatabaseDataModel/DatabaseDataModel.js'; +export * from './DatabaseDataModel/DatabaseDataSource.js'; +export * from './DatabaseDataModel/IDatabaseDataAction.js'; +export * from './DatabaseDataModel/IDatabaseDataActions.js'; +export * from './DatabaseDataModel/IDatabaseDataEditor.js'; +export * from './DatabaseDataModel/IDatabaseDataModel.js'; +export * from './DatabaseDataModel/IDatabaseDataOptions.js'; +export * from './DatabaseDataModel/IDatabaseDataResult.js'; +export * from './DatabaseDataModel/IDatabaseDataSource.js'; +export * from './DatabaseDataModel/IDatabaseResultSet.js'; +export * from './DatabaseDataModel/Order.js'; +export * from './DataViewerService.js'; +export * from './useDataViewerModel.js'; // All Services and Components that is provided by this plugin should be exported here -export * from './TableViewer/TableViewerStorageService'; -export * from './TableViewer/ValuePanel/DataValuePanelService'; +export * from './TableViewer/TableViewerStorageService.js'; +export * from './TableViewer/ValuePanel/DataValuePanelService.js'; -export * from './TableViewer/IDataTableActions'; -export * from './TableViewer/IDataPresentationActions'; +export * from './TableViewer/IDataTableActions.js'; +export * from './TableViewer/IDataPresentationActions.js'; -export * from './TableViewer/TableViewerLoader'; -export * from './TableViewer/TableFooter/TableFooterMenu/DATA_VIEWER_DATA_MODEL_ACTIONS_MENU'; -export * from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService'; +export * from './TableViewer/TableViewerLoader.js'; +export * from './TableViewer/TableFooter/TableFooterMenu/DATA_VIEWER_DATA_MODEL_ACTIONS_MENU.js'; +export * from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.js'; -export * from './ContainerDataSource'; -export * from './DataPresentationService'; -export * from './DataViewerDataChangeConfirmationService'; -export * from './useDataModel'; -export * from './ValuePanelPresentation/BooleanValue/isBooleanValuePresentationAvailable'; -export * from './DataViewerSettingsService'; -export * from './DATA_EDITOR_SETTINGS_GROUP'; +export * from './ContainerDataSource.js'; +export * from './ResultSet/ResultSetDataSource.js'; +export * from './ResultSet/isResultSetDataModel.js'; +export * from './DataPresentationService.js'; +export * from './DataViewerDataChangeConfirmationService.js'; +export * from './ValuePanelPresentation/BooleanValue/isBooleanValuePresentationAvailable.js'; +export * from './useDataViewerCopyHandler.js'; +export * from './DataViewerSettingsService.js'; +export * from './DATA_EDITOR_SETTINGS_GROUP.js'; +export * from './MENU_DV_CONTEXT_MENU.js'; diff --git a/webapp/packages/plugin-data-viewer/src/locales/de.ts b/webapp/packages/plugin-data-viewer/src/locales/de.ts new file mode 100644 index 0000000000..fc9b046096 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/locales/de.ts @@ -0,0 +1,46 @@ +export default [ + ['table_header_sql_expression_not_supported', 'Datenfilter wird nicht unterstützt'], + ['data_viewer_tab_title', 'Daten'], + ['data_viewer_value_edit', 'Bearbeiten'], + ['data_viewer_value_apply', 'Anwenden'], + ['data_viewer_value_revert', 'Abbrechen'], + ['data_viewer_value_revert_title', 'Nicht gespeicherte Änderungen abbrechen'], + ['data_viewer_nodata_message', 'Keine Daten zu zeigen'], + ['data_viewer_statistics_status', 'Status:'], + ['data_viewer_statistics_duration', 'Dauer:'], + ['data_viewer_statistics_updated_rows', 'Zeilen aktualisiert:'], + ['data_viewer_action_refresh', 'Aktualisierung'], + ['data_viewer_action_refresh_tooltip', 'Refresh the data'], + ['data_viewer_action_auto_refresh_stop_tooltip', 'Stop auto-refresh'], + ['data_viewer_action_auto_refresh_menu_tooltip', 'Auto-refresh settings'], + ['data_viewer_action_auto_refresh_menu_stop_tooltip', 'Stop auto-refreshing data'], + ['data_viewer_action_auto_refresh_menu_configure_tooltip', 'Configure auto-refresh settings'], + ['data_viewer_action_auto_refresh_interval_tooltip', 'Set auto-refresh interval to {arg:interval}'], + ['data_viewer_action_edit_delete', 'Ausgewählte löschen'], + ['data_viewer_action_edit_add', 'Hinzufügen'], + ['data_viewer_action_edit_add_copy', 'Duplikat'], + ['data_viewer_action_edit_revert', 'Ausgewählt abbrechen'], + ['data_viewer_result_edited_title', 'Änderungen speichern'], + ['data_viewer_result_edited_message', 'Das Ergebnissatz wurde bearbeitet. Möchten Sie Änderungen in der Datenbank speichern?'], + ['data_viewer_data_save_error_title', 'Fehler beim Speichern von Änderungen trat ein Fehler auf'], + ['plugin_data_viewer_auto_refresh_settings', 'Auto-Refresh Settings'], + ['plugin_data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], + ['data_viewer_presentation_value_title', 'Wert'], + ['data_viewer_presentation_value_text_line_wrapping_wrap', 'Linien wickeln'], + ['data_viewer_presentation_value_image_title', 'Bild'], + ['data_viewer_presentation_value_image_original_size', 'Originalgröße'], + ['data_viewer_presentation_value_boolean_placeholder', 'Der aktuelle Wert kann nicht als Boolean zeigen'], + ['data_viewer_presentation_value_content_paste_error', 'Volltext kann nicht geladen werden'], + ['data_viewer_script_preview_dialog_title', 'Vorschau ändert sich'], + ['data_viewer_script_preview_error_title', 'Kann das Skript nicht bekommen'], + ['data_viewer_total_count_tooltip', 'Totalzahl erhalten'], + ['data_viewer_model_not_loaded', 'Das Tabellenmodell ist nicht geladen'], + ['data_viewer_copy_not_allowed', 'An ability to copy data is disabled'], + ['data_viewer_copy_not_allowed_message', 'If this was unexpected, contact the administrator'], + ['settings_data_editor', 'Dateneditor'], + ['settings_data_editor_fetch_max_name', 'Maximale fetch size'], + ['settings_data_editor_fetch_max_description', 'Maximale Anzahl von Zeilen zum Abrufen'], + ['settings_data_editor_fetch_default_name', 'Standard fetch size'], + ['settings_data_editor_fetch_default_description', 'Standardnummer der Zeilen zum Abrufen'], + ['plugin_data_viewer_no_available_presentation', 'Keine verfügbare Präsentation'], +]; diff --git a/webapp/packages/plugin-data-viewer/src/locales/en.ts b/webapp/packages/plugin-data-viewer/src/locales/en.ts index 52d7d73ace..e829aae5ce 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/en.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/en.ts @@ -1,52 +1,73 @@ export default [ + ['plugin_data_viewer_data_viewer_settings_group', 'Data Editor'], ['table_header_sql_expression', 'Enter a SQL expression to filter results, e.g. column_name=10'], ['table_header_sql_expression_not_supported', 'Data filter is not supported'], ['data_viewer_tab_title', 'Data'], ['data_viewer_value_edit', 'Edit'], ['data_viewer_value_apply', 'Apply'], - ['data_viewer_value_revert', 'Revert'], - ['data_viewer_value_revert_title', 'Revert unsaved changes'], + ['data_viewer_value_revert', 'Cancel'], + ['data_viewer_value_revert_title', 'Cancel unsaved changes'], ['data_viewer_nodata_message', 'No data to show'], ['data_viewer_statistics_status', 'Status:'], ['data_viewer_statistics_duration', 'Duration:'], ['data_viewer_statistics_updated_rows', 'Updated Rows:'], ['data_viewer_action_refresh', 'Refresh'], - ['data_viewer_action_auto_refresh', 'Auto-Refresh'], - ['data_viewer_action_auto_refresh_stop', 'Stop auto-refresh'], + ['data_viewer_action_refresh_tooltip', 'Refresh the data'], + ['data_viewer_action_auto_refresh_stop_tooltip', 'Stop auto-refresh'], + ['data_viewer_action_auto_refresh_menu_tooltip', 'Auto-refresh settings'], + ['data_viewer_action_auto_refresh_menu_stop_tooltip', 'Stop auto-refreshing data'], + ['data_viewer_action_auto_refresh_menu_configure_tooltip', 'Configure auto-refresh settings'], + ['data_viewer_action_auto_refresh_interval_tooltip', 'Set auto-refresh interval to {arg:interval}'], ['data_viewer_action_edit_delete', 'Delete selected'], ['data_viewer_action_edit_add', 'Add'], ['data_viewer_action_edit_add_copy', 'Duplicate'], - ['data_viewer_action_edit_revert', 'Revert selected'], + ['data_viewer_action_edit_revert', 'Cancel selected'], ['data_viewer_result_edited_title', 'Save changes'], ['data_viewer_result_edited_message', 'Result set was edited. Do you want to save changes in database?'], ['data_viewer_data_save_error_title', 'Error occurred while saving changes'], - ['data_viewer_auto_refresh_settings', 'Auto refresh Settings'], - ['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], + ['plugin_data_viewer_auto_refresh_settings', 'Auto-Refresh Settings'], + ['plugin_data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], + ['data_viewer_presentation_value_no_active_elements', 'No selected table cells'], ['data_viewer_presentation_value_title', 'Value'], + ['data_viewer_presentation_value_text_line_wrapping_wrap', 'Wrap lines'], + ['data_viewer_presentation_value_text_line_wrapping_no_wrap', "Don't wrap lines"], ['data_viewer_presentation_value_text_title', 'Text'], ['data_viewer_presentation_value_text_plain_title', 'Text'], ['data_viewer_presentation_value_text_html_title', 'HTML'], ['data_viewer_presentation_value_text_xml_title', 'XML'], ['data_viewer_presentation_value_text_json_title', 'JSON'], + ['data_viewer_presentation_value_text_hex_title', 'HEX'], + ['data_viewer_presentation_value_text_base64_title', 'Base64'], ['data_viewer_presentation_value_image_title', 'Image'], ['data_viewer_presentation_value_image_fit', 'Fit Window'], ['data_viewer_presentation_value_image_original_size', 'Original Size'], ['data_viewer_presentation_value_boolean_placeholder', "Can't show current value as boolean"], - ['data_viewer_presentation_value_content_truncated_placeholder', 'The size of the value exceeds the'], - ['data_viewer_presentation_value_content_was_truncated', 'The value was truncated'], - ['data_viewer_presentation_value_content_value_size', 'Value size'], + ['data_viewer_presentation_value_content_truncated_placeholder', 'The value was truncated because of the'], ['data_viewer_presentation_value_content_download_error', 'Download failed'], + ['data_viewer_presentation_value_content_paste_error', 'Cannot load full text'], ['data_viewer_script_preview', 'Script'], ['data_viewer_script_preview_dialog_title', 'Preview changes'], ['data_viewer_script_preview_error_title', "Can't get the script"], ['data_viewer_refresh_result_set', 'Refresh result set'], + ['data_viewer_total_count_tooltip', 'Get total count'], + ['data_viewer_total_count_canceled_title', 'Total count canceled'], + ['data_viewer_total_count_canceled_message', 'Statement was cancelled due to user request'], + ['data_viewer_total_count_failed', 'Failed to get total count'], + ['data_viewer_model_not_loaded', 'Table model is not loaded'], + ['data_viewer_copy_not_allowed', 'An ability to copy data is disabled'], + ['data_viewer_copy_not_allowed_message', 'If this was unexpected, contact the administrator'], ['settings_data_editor', 'Data Editor'], - ['settings_data_editor_disable_edit_name', 'Disable Edit'], - ['settings_data_editor_disable_edit_description', 'Disable Edit'], - ['settings_data_editor_fetch_min_name', 'Minimum fetch size'], - ['settings_data_editor_fetch_min_description', 'Minimum number of rows to fetch'], ['settings_data_editor_fetch_max_name', 'Maximum fetch size'], ['settings_data_editor_fetch_max_description', 'Maximum number of rows to fetch'], ['settings_data_editor_fetch_default_name', 'Default fetch size'], ['settings_data_editor_fetch_default_description', 'Default number of rows to fetch'], + ['plugin_data_viewer_no_available_presentation', 'No available presentation'], + ['plugin_data_viewer_result_set_save_success', 'Saved successfully'], + ['plugin_data_viewer_sql_session_closed_title', 'SQL Execution Session Closed'], + [ + 'plugin_data_viewer_sql_session_closed_message', + 'The current SQL execution session has been closed, possibly due to connection timeout or manual disconnection. You can reopen the SQL editor and retain the current query statement.', + ], + ['plugin_data_viewer_sql_session_closed_query_label', 'Current Query:'], + ['plugin_data_viewer_sql_session_closed_open_editor', 'Reopen Editor'], ]; diff --git a/webapp/packages/plugin-data-viewer/src/locales/fr.ts b/webapp/packages/plugin-data-viewer/src/locales/fr.ts new file mode 100644 index 0000000000..4f23aed7c6 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/locales/fr.ts @@ -0,0 +1,64 @@ +export default [ + ['plugin_data_viewer_data_viewer_settings_group', 'Editeur de données'], + ['table_header_sql_expression', 'Entrez une expression SQL pour filtrer les résultats, par exemple column_name=10'], + ['table_header_sql_expression_not_supported', "Le filtrage des données n'est pas supporté"], + ['data_viewer_tab_title', 'Données'], + ['data_viewer_value_edit', 'Modifier'], + ['data_viewer_value_apply', 'Appliquer'], + ['data_viewer_value_revert', 'Annuler'], + ['data_viewer_value_revert_title', 'Annuler les modifications non sauvegardées'], + ['data_viewer_nodata_message', 'Aucune donnée à afficher'], + ['data_viewer_statistics_status', 'Statut :'], + ['data_viewer_statistics_duration', 'Durée :'], + ['data_viewer_statistics_updated_rows', 'Lignes mises à jour :'], + ['data_viewer_action_refresh', 'Actualiser'], + ['data_viewer_action_refresh_tooltip', 'Refresh the data'], + ['data_viewer_action_auto_refresh_stop_tooltip', 'Stop auto-refresh'], + ['data_viewer_action_auto_refresh_menu_tooltip', 'Auto-refresh settings'], + ['data_viewer_action_auto_refresh_menu_stop_tooltip', 'Stop auto-refreshing data'], + ['data_viewer_action_auto_refresh_menu_configure_tooltip', 'Configure auto-refresh settings'], + ['data_viewer_action_auto_refresh_interval_tooltip', 'Set auto-refresh interval to {arg:interval}'], + ['data_viewer_action_edit_delete', 'Supprimer la sélection'], + ['data_viewer_action_edit_add', 'Ajouter'], + ['data_viewer_action_edit_add_copy', 'Dupliquer'], + ['data_viewer_action_edit_revert', 'Annuler la sélection'], + ['data_viewer_result_edited_title', 'Enregistrer les modifications'], + ['data_viewer_result_edited_message', 'Le jeu de résultats a été modifié. Voulez-vous enregistrer les modifications dans la base de données ?'], + ['data_viewer_data_save_error_title', "Erreur lors de l'enregistrement des modifications"], + ['plugin_data_viewer_auto_refresh_settings', 'Auto-Refresh Settings'], + ['plugin_data_viewer_auto_refresh_settings_stop_on_error', "Arrêter en cas d'erreur"], + ['data_viewer_presentation_value_title', 'Valeur'], + ['data_viewer_presentation_value_text_line_wrapping_wrap', 'Retour à la ligne'], + ['data_viewer_presentation_value_text_line_wrapping_no_wrap', 'Ne pas revenir à la ligne'], + ['data_viewer_presentation_value_text_title', 'Texte'], + ['data_viewer_presentation_value_text_plain_title', 'Texte'], + ['data_viewer_presentation_value_text_html_title', 'HTML'], + ['data_viewer_presentation_value_text_xml_title', 'XML'], + ['data_viewer_presentation_value_text_json_title', 'JSON'], + ['data_viewer_presentation_value_text_hex_title', 'HEX'], + ['data_viewer_presentation_value_text_base64_title', 'Base64'], + ['data_viewer_presentation_value_image_title', 'Image'], + ['data_viewer_presentation_value_image_fit', 'Ajuster à la fenêtre'], + ['data_viewer_presentation_value_image_original_size', "Taille d'origine"], + ['data_viewer_presentation_value_boolean_placeholder', "Impossible d'afficher la valeur actuelle en tant que booléen"], + ['data_viewer_presentation_value_content_truncated_placeholder', 'La valeur a été tronquée en raison de la'], + ['data_viewer_presentation_value_content_download_error', 'Échec du téléchargement'], + ['data_viewer_presentation_value_content_paste_error', 'Impossible de charger le texte complet'], + ['data_viewer_script_preview', 'Script'], + ['data_viewer_script_preview_dialog_title', 'Aperçu des modifications'], + ['data_viewer_script_preview_error_title', "Impossible d'obtenir le script"], + ['data_viewer_refresh_result_set', 'Actualiser le jeu de résultats'], + ['data_viewer_total_count_tooltip', 'Obtenir le compte total'], + ['data_viewer_total_count_canceled_title', 'Total annulé'], + ['data_viewer_total_count_canceled_message', "La déclaration a été annulée à la demande de l'utilisateur"], + ['data_viewer_total_count_failed', "Échec de l'obtention du compte total"], + ['data_viewer_model_not_loaded', "Le modèle de la table n'est pas chargé"], + ['data_viewer_copy_not_allowed', 'An ability to copy data is disabled'], + ['data_viewer_copy_not_allowed_message', 'If this was unexpected, contact the administrator'], + ['settings_data_editor', 'Éditeur de données'], + ['settings_data_editor_fetch_max_name', 'Taille de récupération maximale'], + ['settings_data_editor_fetch_max_description', 'Nombre maximal de lignes à récupérer'], + ['settings_data_editor_fetch_default_name', 'Taille de récupération par défaut'], + ['settings_data_editor_fetch_default_description', 'Nombre par défaut de lignes à récupérer'], + ['plugin_data_viewer_no_available_presentation', 'Aucune présentation disponible'], +]; diff --git a/webapp/packages/plugin-data-viewer/src/locales/it.ts b/webapp/packages/plugin-data-viewer/src/locales/it.ts index 87b17b2405..e7702ac6b6 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/it.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/it.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_data_viewer_data_viewer_settings_group', 'Data Editor'], ['table_header_sql_expression', 'Digita una espresione SQL per filtrare i risultati'], ['table_header_sql_expression_not_supported', 'Data filter is not supported'], ['data_viewer_tab_title', 'Dati'], @@ -11,35 +12,48 @@ export default [ ['data_viewer_statistics_duration', 'Durata:'], ['data_viewer_statistics_updated_rows', 'Righe Aggiornate:'], ['data_viewer_action_refresh', 'Refresh'], - ['data_viewer_action_auto_refresh', 'Auto-Refresh'], - ['data_viewer_action_auto_refresh_stop', 'Stop auto-refresh'], + ['data_viewer_action_refresh_tooltip', 'Refresh the data'], + ['data_viewer_action_auto_refresh_stop_tooltip', 'Stop auto-refresh'], + ['data_viewer_action_auto_refresh_menu_tooltip', 'Auto-refresh settings'], + ['data_viewer_action_auto_refresh_menu_stop_tooltip', 'Stop auto-refreshing data'], + ['data_viewer_action_auto_refresh_menu_configure_tooltip', 'Configure auto-refresh settings'], + ['data_viewer_action_auto_refresh_interval_tooltip', 'Set auto-refresh interval to {arg:interval}'], ['data_viewer_result_edited_title', 'Save changes'], ['data_viewer_result_edited_message', 'Result set was edited. Do you want to save changes in database?'], ['data_viewer_data_save_error_title', 'Error occurred while saving changes'], - ['data_viewer_auto_refresh_settings', 'Auto refresh Settings'], - ['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], + ['plugin_data_viewer_auto_refresh_settings', 'Auto-Refresh Settings'], + ['plugin_data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], + ['data_viewer_presentation_value_no_active_elements', 'No selected table cells'], ['data_viewer_presentation_value_title', 'Valore'], + ['data_viewer_presentation_value_text_line_wrapping_wrap', 'Wrap lines'], + ['data_viewer_presentation_value_text_line_wrapping_no_wrap', "Don't wrap lines"], ['data_viewer_presentation_value_text_title', 'Testo'], ['data_viewer_presentation_value_text_plain_title', 'Testo'], ['data_viewer_presentation_value_text_html_title', 'HTML'], ['data_viewer_presentation_value_text_xml_title', 'XML'], ['data_viewer_presentation_value_text_json_title', 'JSON'], + ['data_viewer_presentation_value_text_hex_title', 'HEX'], + ['data_viewer_presentation_value_text_base64_title', 'Base64'], ['data_viewer_presentation_value_image_title', 'Immagine'], ['data_viewer_presentation_value_image_fit', 'Adatta alla Finestra'], ['data_viewer_presentation_value_image_original_size', 'Dimensioni Originali'], ['data_viewer_presentation_value_boolean_placeholder', 'Non posso rappresentare il valore corrente come booleano'], - ['data_viewer_presentation_value_content_truncated_placeholder', 'The size of the value exceeds the'], - ['data_viewer_presentation_value_content_was_truncated', 'The value was truncated'], - ['data_viewer_presentation_value_content_value_size', 'Value size'], + ['data_viewer_presentation_value_content_truncated_placeholder', 'The value was truncated because of the'], ['data_viewer_presentation_value_content_download_error', 'Download failed'], + ['data_viewer_presentation_value_content_paste_error', 'Cannot load full text'], ['data_viewer_refresh_result_set', 'Refresh result set'], + ['data_viewer_total_count_tooltip', 'Get total count'], + ['data_viewer_total_count_canceled_title', 'Total count canceled'], + ['data_viewer_total_count_canceled_message', 'Statement was cancelled due to user request'], + ['data_viewer_total_count_failed', 'Failed to get total count'], + ['data_viewer_model_not_loaded', 'Table model is not loaded'], + ['data_viewer_copy_not_allowed', 'An ability to copy data is disabled'], + ['data_viewer_copy_not_allowed_message', 'If this was unexpected, contact the administrator'], ['settings_data_editor', 'Data Editor'], - ['settings_data_editor_disable_edit_name', 'Disable Edit'], - ['settings_data_editor_disable_edit_description', 'Disable Edit'], - ['settings_data_editor_fetch_min_name', 'Minimum fetch size'], - ['settings_data_editor_fetch_min_description', 'Minimum number of rows to fetch'], ['settings_data_editor_fetch_max_name', 'Maximum fetch size'], ['settings_data_editor_fetch_max_description', 'Maximum number of rows to fetch'], ['settings_data_editor_fetch_default_name', 'Default fetch size'], ['settings_data_editor_fetch_default_description', 'Default number of rows to fetch'], + ['plugin_data_viewer_no_available_presentation', 'No available presentation'], + ['plugin_data_viewer_result_set_save_success', 'Saved successfully'], ]; diff --git a/webapp/packages/plugin-data-viewer/src/locales/ru.ts b/webapp/packages/plugin-data-viewer/src/locales/ru.ts index d98434d3e0..de3479de31 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/ru.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/ru.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_data_viewer_data_viewer_settings_group', 'Редактор данных'], ['table_header_sql_expression', 'Введите SQL выражение чтобы отфильтровать результаты, например, column_name=10'], ['table_header_sql_expression_not_supported', 'Фильтрация данных недоступна'], ['data_viewer_tab_title', 'Данные'], @@ -11,8 +12,12 @@ export default [ ['data_viewer_statistics_duration', 'Длительность:'], ['data_viewer_statistics_updated_rows', 'Обновлено строк:'], ['data_viewer_action_refresh', 'Обновить'], - ['data_viewer_action_auto_refresh', 'Автообновление'], - ['data_viewer_action_auto_refresh_stop', 'Остановить автообновление'], + ['data_viewer_action_refresh_tooltip', 'Обновить данные'], + ['data_viewer_action_auto_refresh_stop_tooltip', 'Остановить автоматическое обновление'], + ['data_viewer_action_auto_refresh_menu_tooltip', 'Настройки автоматического обновления'], + ['data_viewer_action_auto_refresh_menu_stop_tooltip', 'Остановить автоматическое обновление данных'], + ['data_viewer_action_auto_refresh_menu_configure_tooltip', 'Настроить параметры автоматического обновления'], + ['data_viewer_action_auto_refresh_interval_tooltip', 'Установить интервал автоматического обновления в {arg:interval}'], ['data_viewer_action_edit_delete', 'Удалить'], ['data_viewer_action_edit_add', 'Добавить'], ['data_viewer_action_edit_add_copy', 'Дублировать'], @@ -20,29 +25,36 @@ export default [ ['data_viewer_result_edited_title', 'Сохранить изменения'], ['data_viewer_result_edited_message', 'Данные результата запроса были изменены. Вы хотите сохранить изменения?'], ['data_viewer_data_save_error_title', 'Ошибка сохранения изменений'], - ['data_viewer_auto_refresh_settings', 'Параметры автообновления'], - ['data_viewer_auto_refresh_settings_stop_on_error', 'Остановить при ошибке'], + ['plugin_data_viewer_auto_refresh_settings', 'Параметры автообновления'], + ['plugin_data_viewer_auto_refresh_settings_stop_on_error', 'Остановить при ошибке'], + ['data_viewer_presentation_value_no_active_elements', 'Нет выбраных ячеек таблицы'], ['data_viewer_presentation_value_title', 'Значение'], + ['data_viewer_presentation_value_text_line_wrapping_wrap', 'Переносить строки'], + ['data_viewer_presentation_value_text_line_wrapping_no_wrap', 'Не переносить строки'], ['data_viewer_presentation_value_text_title', 'Текст'], ['data_viewer_presentation_value_image_title', 'Изображение'], ['data_viewer_presentation_value_image_fit', 'Растянуть'], ['data_viewer_presentation_value_image_original_size', 'Оригинальный размер'], ['data_viewer_presentation_value_boolean_placeholder', 'Не удалось отобразить текущее значение как boolean'], - ['data_viewer_presentation_value_content_truncated_placeholder', 'Размер значения превышает'], - ['data_viewer_presentation_value_content_was_truncated', 'Значение было обрезано'], - ['data_viewer_presentation_value_content_value_size', 'Размер значения'], + ['data_viewer_presentation_value_content_truncated_placeholder', 'Значение обрезано потому что превышает'], ['data_viewer_presentation_value_content_download_error', 'Не удалось загрузить файл'], + ['data_viewer_presentation_value_content_paste_error', 'Не удалось загрузить весь текст'], ['data_viewer_script_preview', 'Скрипт'], ['data_viewer_script_preview_dialog_title', 'Предпросмотр изменений'], ['data_viewer_script_preview_error_title', 'Не удалось получить скрипт'], ['data_viewer_refresh_result_set', 'Обновить резалт сет'], + ['data_viewer_total_count_tooltip', 'Получить количество записей'], + ['data_viewer_total_count_canceled_title', 'Получение количества записей отменено'], + ['data_viewer_total_count_canceled_message', 'Запрос был отменен пользователем'], + ['data_viewer_total_count_failed', 'Не удалось получить количество записей'], + ['data_viewer_model_not_loaded', 'Не удалось загрузить модель таблицы'], + ['data_viewer_copy_not_allowed', 'Копирование отключено'], + ['data_viewer_copy_not_allowed_message', 'Если это не ожидаемо, обратитесь к администратору'], ['settings_data_editor', 'Редактор данных'], - ['settings_data_editor_disable_edit_name', 'Отключить редактирование'], - ['settings_data_editor_disable_edit_description', 'Отключить редактирование'], - ['settings_data_editor_fetch_min_name', 'Минимальный размер выборки'], - ['settings_data_editor_fetch_min_description', 'Минимальное количество строк для выборки'], ['settings_data_editor_fetch_max_name', 'Максимальный размер выборки'], ['settings_data_editor_fetch_max_description', 'Максимальное количество строк для выборки'], ['settings_data_editor_fetch_default_name', 'Размер выборки по умолчанию'], ['settings_data_editor_fetch_default_description', 'Количество строк для выборки по умолчанию'], + ['plugin_data_viewer_no_available_presentation', 'Нет доступных представлений'], + ['plugin_data_viewer_result_set_save_success', 'Успешно сохранено'], ]; diff --git a/webapp/packages/plugin-data-viewer/src/locales/vi.ts b/webapp/packages/plugin-data-viewer/src/locales/vi.ts new file mode 100644 index 0000000000..b6366eed56 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/locales/vi.ts @@ -0,0 +1,66 @@ +export default [ + ['plugin_data_viewer_data_viewer_settings_group', 'Trình chỉnh sửa dữ liệu'], + ['table_header_sql_expression', 'Nhập biểu thức SQL để lọc kết quả, ví dụ: column_name=10'], + ['table_header_sql_expression_not_supported', 'Lọc dữ liệu không được hỗ trợ'], + ['data_viewer_tab_title', 'Dữ liệu'], + ['data_viewer_value_edit', 'Chỉnh sửa'], + ['data_viewer_value_apply', 'Áp dụng'], + ['data_viewer_value_revert', 'Hủy'], + ['data_viewer_value_revert_title', 'Hủy các thay đổi chưa lưu'], + ['data_viewer_nodata_message', 'Không có dữ liệu để hiển thị'], + ['data_viewer_statistics_status', 'Trạng thái:'], + ['data_viewer_statistics_duration', 'Thời gian:'], + ['data_viewer_statistics_updated_rows', 'Số hàng đã cập nhật:'], + ['data_viewer_action_refresh', 'Làm mới'], + ['data_viewer_action_refresh_tooltip', 'Làm mới dữ liệu'], + ['data_viewer_action_auto_refresh_stop_tooltip', 'Dừng làm mới tự động'], + ['data_viewer_action_auto_refresh_menu_tooltip', 'Cài đặt làm mới tự động'], + ['data_viewer_action_auto_refresh_menu_stop_tooltip', 'Dừng làm mới dữ liệu tự động'], + ['data_viewer_action_auto_refresh_menu_configure_tooltip', 'Cấu hình cài đặt làm mới tự động'], + ['data_viewer_action_auto_refresh_interval_tooltip', 'Đặt khoảng thời gian làm mới tự động thành {arg:interval}'], + ['data_viewer_action_edit_delete', 'Xóa mục đã chọn'], + ['data_viewer_action_edit_add', 'Thêm'], + ['data_viewer_action_edit_add_copy', 'Sao chép'], + ['data_viewer_action_edit_revert', 'Hủy mục đã chọn'], + ['data_viewer_result_edited_title', 'Lưu thay đổi'], + ['data_viewer_result_edited_message', 'Tập hợp kết quả đã được chỉnh sửa. Bạn có muốn lưu thay đổi vào cơ sở dữ liệu không?'], + ['data_viewer_data_save_error_title', 'Xảy ra lỗi khi lưu thay đổi'], + ['plugin_data_viewer_auto_refresh_settings', 'Cài đặt làm mới tự động'], + ['plugin_data_viewer_auto_refresh_settings_stop_on_error', 'Dừng khi gặp lỗi'], + ['data_viewer_presentation_value_no_active_elements', 'Không có ô bảng nào được chọn'], + ['data_viewer_presentation_value_title', 'Giá trị'], + ['data_viewer_presentation_value_text_line_wrapping_wrap', 'Ngắt dòng'], + ['data_viewer_presentation_value_text_line_wrapping_no_wrap', 'Không ngắt dòng'], + ['data_viewer_presentation_value_text_title', 'Văn bản'], + ['data_viewer_presentation_value_text_plain_title', 'Văn bản'], + ['data_viewer_presentation_value_text_html_title', 'HTML'], + ['data_viewer_presentation_value_text_xml_title', 'XML'], + ['data_viewer_presentation_value_text_json_title', 'JSON'], + ['data_viewer_presentation_value_text_hex_title', 'HEX'], + ['data_viewer_presentation_value_text_base64_title', 'Base64'], + ['data_viewer_presentation_value_image_title', 'Hình ảnh'], + ['data_viewer_presentation_value_image_fit', 'Vừa cửa sổ'], + ['data_viewer_presentation_value_image_original_size', 'Kích thước gốc'], + ['data_viewer_presentation_value_boolean_placeholder', 'Không thể hiển thị giá trị hiện tại dưới dạng boolean'], + ['data_viewer_presentation_value_content_truncated_placeholder', 'Giá trị đã bị cắt bớt do'], + ['data_viewer_presentation_value_content_download_error', 'Tải xuống thất bại'], + ['data_viewer_presentation_value_content_paste_error', 'Không thể tải toàn bộ văn bản'], + ['data_viewer_script_preview', 'Kịch bản'], + ['data_viewer_script_preview_dialog_title', 'Xem trước thay đổi'], + ['data_viewer_script_preview_error_title', 'Không thể lấy kịch bản'], + ['data_viewer_refresh_result_set', 'Làm mới tập hợp kết quả'], + ['data_viewer_total_count_tooltip', 'Lấy tổng số lượng'], + ['data_viewer_total_count_canceled_title', 'Đã hủy tổng số lượng'], + ['data_viewer_total_count_canceled_message', 'Câu lệnh đã bị hủy theo yêu cầu của người dùng'], + ['data_viewer_total_count_failed', 'Không thể lấy tổng số lượng'], + ['data_viewer_model_not_loaded', 'Mô hình bảng chưa được tải'], + ['data_viewer_copy_not_allowed', 'Khả năng sao chép dữ liệu đã bị tắt'], + ['data_viewer_copy_not_allowed_message', 'Nếu điều này bất ngờ, vui lòng liên hệ quản trị viên'], + ['settings_data_editor', 'Trình chỉnh sửa dữ liệu'], + ['settings_data_editor_fetch_max_name', 'Kích thước lấy tối đa'], + ['settings_data_editor_fetch_max_description', 'Số lượng hàng tối đa để lấy'], + ['settings_data_editor_fetch_default_name', 'Kích thước lấy mặc định'], + ['settings_data_editor_fetch_default_description', 'Số lượng hàng mặc định để lấy'], + ['plugin_data_viewer_no_available_presentation', 'Không có chế độ hiển thị nào khả dụng'], + ['plugin_data_viewer_result_set_save_success', 'Lưu thành công'], +]; diff --git a/webapp/packages/plugin-data-viewer/src/locales/zh.ts b/webapp/packages/plugin-data-viewer/src/locales/zh.ts index 40470747a3..8335e82a5c 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/zh.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/zh.ts @@ -1,6 +1,7 @@ export default [ + ['plugin_data_viewer_data_viewer_settings_group', '資料編輯器'], ['table_header_sql_expression', '输入SQL表达式以过滤结果'], - ['table_header_sql_expression_not_supported', 'Data filter is not supported'], + ['table_header_sql_expression_not_supported', '不支持数据过滤器'], ['data_viewer_tab_title', '数据'], ['data_viewer_value_edit', '编辑'], ['data_viewer_value_apply', '应用'], @@ -10,43 +11,56 @@ export default [ ['data_viewer_statistics_status', '状态:'], ['data_viewer_statistics_duration', '用时:'], ['data_viewer_statistics_updated_rows', '更新行:'], - ['data_viewer_action_refresh', 'Refresh'], - ['data_viewer_action_auto_refresh', 'Auto-Refresh'], - ['data_viewer_action_auto_refresh_stop', 'Stop auto-refresh'], + ['data_viewer_action_refresh', '刷新'], + ['data_viewer_action_refresh_tooltip', '刷新数据'], + ['data_viewer_action_auto_refresh_stop_tooltip', '停止自动刷新'], + ['data_viewer_action_auto_refresh_menu_tooltip', '自动刷新设置'], + ['data_viewer_action_auto_refresh_menu_stop_tooltip', '停止自动刷新数据'], + ['data_viewer_action_auto_refresh_menu_configure_tooltip', '配置自动刷新'], + ['data_viewer_action_auto_refresh_interval_tooltip', '自动刷新间隔设置为 {arg:interval}'], ['data_viewer_action_edit_delete', '删除选中'], ['data_viewer_action_edit_add', '添加'], - ['data_viewer_action_edit_add_copy', 'Duplicate'], + ['data_viewer_action_edit_add_copy', '复制并添加行'], ['data_viewer_action_edit_revert', '还原选中'], ['data_viewer_result_edited_title', '保存更改'], ['data_viewer_result_edited_message', '结果集已编辑。是否将更改保存到数据库?'], - ['data_viewer_data_save_error_title', 'Error occurred while saving changes'], - ['data_viewer_auto_refresh_settings', 'Auto refresh Settings'], - ['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'], + ['data_viewer_data_save_error_title', '保存更改时出错'], + ['plugin_data_viewer_auto_refresh_settings', '自动刷新设置'], + ['plugin_data_viewer_auto_refresh_settings_stop_on_error', '出错时停止'], + ['data_viewer_presentation_value_no_active_elements', '未选择表单元格'], ['data_viewer_presentation_value_title', '值'], + ['data_viewer_presentation_value_text_line_wrapping_wrap', '换行'], + ['data_viewer_presentation_value_text_line_wrapping_no_wrap', '不用换行'], ['data_viewer_presentation_value_text_title', '文本'], ['data_viewer_presentation_value_text_plain_title', '文本'], ['data_viewer_presentation_value_text_html_title', 'HTML'], ['data_viewer_presentation_value_text_xml_title', 'XML'], ['data_viewer_presentation_value_text_json_title', 'JSON'], + ['data_viewer_presentation_value_text_hex_title', 'HEX'], + ['data_viewer_presentation_value_text_base64_title', 'Base64'], ['data_viewer_presentation_value_image_title', '图片'], ['data_viewer_presentation_value_image_fit', '适应窗口'], ['data_viewer_presentation_value_image_original_size', '原始尺寸'], ['data_viewer_presentation_value_boolean_placeholder', '无法将当前值显示为布尔值'], - ['data_viewer_presentation_value_content_truncated_placeholder', 'The size of the value exceeds the'], - ['data_viewer_presentation_value_content_was_truncated', 'The value was truncated'], - ['data_viewer_presentation_value_content_value_size', 'Value size'], - ['data_viewer_presentation_value_content_download_error', 'Download failed'], + ['data_viewer_presentation_value_content_truncated_placeholder', '值被截断,因为'], + ['data_viewer_presentation_value_content_download_error', '下载失败'], + ['data_viewer_presentation_value_content_paste_error', '无法加载全文本'], ['data_viewer_script_preview', '脚本'], ['data_viewer_script_preview_dialog_title', '预览更改'], ['data_viewer_script_preview_error_title', '无法获取脚本'], - ['data_viewer_refresh_result_set', 'Refresh result set'], - ['settings_data_editor', 'Data Editor'], - ['settings_data_editor_disable_edit_name', 'Disable Edit'], - ['settings_data_editor_disable_edit_description', 'Disable Edit'], - ['settings_data_editor_fetch_min_name', 'Minimum fetch size'], - ['settings_data_editor_fetch_min_description', 'Minimum number of rows to fetch'], - ['settings_data_editor_fetch_max_name', 'Maximum fetch size'], - ['settings_data_editor_fetch_max_description', 'Maximum number of rows to fetch'], - ['settings_data_editor_fetch_default_name', 'Default fetch size'], - ['settings_data_editor_fetch_default_description', 'Default number of rows to fetch'], + ['data_viewer_refresh_result_set', '刷新结果设置'], + ['data_viewer_total_count_failed', '获取总数失败'], + ['data_viewer_total_count_tooltip', '获取总数'], + ['data_viewer_total_count_canceled_title', '取消计算总数'], + ['data_viewer_total_count_canceled_message', '由于用户请求,计数已被取消'], + ['data_viewer_model_not_loaded', '表模型未加载'], + ['data_viewer_copy_not_allowed', '复制数据的功能已禁用'], + ['data_viewer_copy_not_allowed_message', '如果这是意外的,请联系管理员'], + ['settings_data_editor', '数据编辑器'], + ['settings_data_editor_fetch_max_name', '最大查询数量'], + ['settings_data_editor_fetch_max_description', '最大查询数据行数'], + ['settings_data_editor_fetch_default_name', '默认查询数量'], + ['settings_data_editor_fetch_default_description', '默认查询数据行数'], + ['plugin_data_viewer_no_available_presentation', '无可用展示项'], + ['plugin_data_viewer_result_set_save_success', '保存成功'], ]; diff --git a/webapp/packages/plugin-data-viewer/src/manifest.ts b/webapp/packages/plugin-data-viewer/src/manifest.ts index 9c600e801c..5153219921 100644 --- a/webapp/packages/plugin-data-viewer/src/manifest.ts +++ b/webapp/packages/plugin-data-viewer/src/manifest.ts @@ -1,52 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { DataPresentationService } from './DataPresentationService'; -import { DataViewerBootstrap } from './DataViewerBootstrap'; -import { DataViewerDataChangeConfirmationService } from './DataViewerDataChangeConfirmationService'; -import { DataViewerService } from './DataViewerService'; -import { DataViewerSettingsService } from './DataViewerSettingsService'; -import { DataViewerTableService } from './DataViewerTableService'; -import { DataViewerTabService } from './DataViewerTabService'; -import { LocaleService } from './LocaleService'; -import { TableFooterMenuService } from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService'; -import { TableHeaderService } from './TableViewer/TableHeader/TableHeaderService'; -import { TableViewerStorageService } from './TableViewer/TableViewerStorageService'; -import { DataValuePanelBootstrap } from './TableViewer/ValuePanel/DataValuePanelBootstrap'; -import { DataValuePanelService } from './TableViewer/ValuePanel/DataValuePanelService'; -import { BooleanValuePresentationBootstrap } from './ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap'; -import { ImageValuePresentationBootstrap } from './ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap'; -import { TextValuePresentationBootstrap } from './ValuePanelPresentation/TextValue/TextValuePresentationBootstrap'; -import { TextValuePresentationService } from './ValuePanelPresentation/TextValue/TextValuePresentationService'; - export const dataViewerManifest: PluginManifest = { info: { - name: 'Data Viewer Plugin', + name: 'Data Editor Plugin', }, - - providers: [ - DataViewerBootstrap, - DataViewerTabService, - DataViewerTableService, - DataPresentationService, - TableViewerStorageService, - TableFooterMenuService, - TableHeaderService, - LocaleService, - DataValuePanelService, - TextValuePresentationService, - DataViewerDataChangeConfirmationService, - TextValuePresentationBootstrap, - ImageValuePresentationBootstrap, - BooleanValuePresentationBootstrap, - DataValuePanelBootstrap, - DataViewerSettingsService, - DataViewerService, - ], }; diff --git a/webapp/packages/plugin-data-viewer/src/module.ts b/webapp/packages/plugin-data-viewer/src/module.ts new file mode 100644 index 0000000000..9339a59301 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/module.ts @@ -0,0 +1,68 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { TextValuePresentationBootstrap } from './ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.js'; +import { TextValuePresentationService } from './ValuePanelPresentation/TextValue/TextValuePresentationService.js'; +import { ImageValuePresentationBootstrap } from './ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.js'; +import { BooleanValuePresentationBootstrap } from './ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap.js'; +import { TableViewerStorageService } from './TableViewer/TableViewerStorageService.js'; +import { DataValuePanelService } from './TableViewer/ValuePanel/DataValuePanelService.js'; +import { DataValuePanelBootstrap } from './TableViewer/ValuePanel/DataValuePanelBootstrap.js'; +import { TableHeaderService } from './TableViewer/TableHeader/TableHeaderService.js'; +import { TableFooterMenuService } from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.js'; +import { TableRefreshActionBootstrap } from './TableViewer/TableFooter/TableFooterMenu/RefreshAction/TableRefreshActionBootstrap.js'; +import { TableFetchSizeActionBootstrap } from './TableViewer/TableFooter/TableFooterMenu/FetchSizeAction/TableFetchSizeActionBootstrap.js'; +import { DataViewerViewService } from './TableViewer/DataViewerViewService.js'; +import { ResultSetTableFooterMenuService } from './ResultSet/ResultSetTableFooterMenuService.js'; +import { LocaleService } from './LocaleService.js'; +import { DataViewerTabService } from './DataViewerTabService.js'; +import { DataViewerTableService } from './DataViewerTableService.js'; +import { DataViewerSettingsService } from './DataViewerSettingsService.js'; +import { DataViewerService } from './DataViewerService.js'; +import { DataViewerDataChangeConfirmationService } from './DataViewerDataChangeConfirmationService.js'; +import { DataViewerBootstrap } from './DataViewerBootstrap.js'; +import { DataPresentationService } from './DataPresentationService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-data-viewer', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, proxy(BooleanValuePresentationBootstrap)) + .addSingleton(Bootstrap, proxy(DataValuePanelBootstrap)) + .addSingleton(Bootstrap, proxy(DataViewerBootstrap)) + .addSingleton(Bootstrap, proxy(ImageValuePresentationBootstrap)) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(Bootstrap, proxy(TableFetchSizeActionBootstrap)) + .addSingleton(Bootstrap, proxy(TableHeaderService)) + .addSingleton(Bootstrap, proxy(TableRefreshActionBootstrap)) + .addSingleton(Bootstrap, proxy(TextValuePresentationBootstrap)) + .addSingleton(Dependency, proxy(DataViewerSettingsService)) + .addSingleton(DataPresentationService) + .addSingleton(TextValuePresentationBootstrap) + .addSingleton(TextValuePresentationService) + .addSingleton(ImageValuePresentationBootstrap) + .addSingleton(BooleanValuePresentationBootstrap) + .addSingleton(TableViewerStorageService) + .addSingleton(DataValuePanelService) + .addSingleton(DataValuePanelBootstrap) + .addSingleton(TableHeaderService) + .addSingleton(TableFooterMenuService) + .addSingleton(TableRefreshActionBootstrap) + .addSingleton(TableFetchSizeActionBootstrap) + .addSingleton(DataViewerViewService) + .addSingleton(ResultSetTableFooterMenuService) + .addSingleton(DataViewerTabService) + .addSingleton(DataViewerTableService) + .addSingleton(DataViewerSettingsService) + .addSingleton(DataViewerService) + .addSingleton(DataViewerDataChangeConfirmationService) + .addSingleton(DataViewerBootstrap); + }, +}); diff --git a/webapp/packages/plugin-data-viewer/src/useDataModel.ts b/webapp/packages/plugin-data-viewer/src/useDataModel.ts deleted file mode 100644 index a2a2e450eb..0000000000 --- a/webapp/packages/plugin-data-viewer/src/useDataModel.ts +++ /dev/null @@ -1,17 +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 { useService } from '@cloudbeaver/core-di'; - -import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel'; -import { TableViewerStorageService } from './TableViewer/TableViewerStorageService'; - -export function useDataModel>(modelId: string): T | undefined { - const dataViewerTableService = useService(TableViewerStorageService); - - return dataViewerTableService.get(modelId); -} diff --git a/webapp/packages/plugin-data-viewer/src/useDataViewerCopyHandler.ts b/webapp/packages/plugin-data-viewer/src/useDataViewerCopyHandler.ts new file mode 100644 index 0000000000..f76d6eb56d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/useDataViewerCopyHandler.ts @@ -0,0 +1,31 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type React from 'react'; + +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; + +import { DataViewerService } from './DataViewerService.js'; + +export function useDataViewerCopyHandler() { + const notificationService = useService(NotificationService); + const dataViewerService = useService(DataViewerService); + + return function (event?: ClipboardEvent | React.KeyboardEvent | React.ClipboardEvent) { + if (!dataViewerService.canCopyData) { + event?.preventDefault(); + + notificationService.logInfo({ + title: 'data_viewer_copy_not_allowed', + message: 'data_viewer_copy_not_allowed_message', + }); + } + + return !dataViewerService.canCopyData; + }; +} diff --git a/webapp/packages/plugin-data-viewer/src/useDataViewerModel.ts b/webapp/packages/plugin-data-viewer/src/useDataViewerModel.ts new file mode 100644 index 0000000000..43e81fbe8b --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/useDataViewerModel.ts @@ -0,0 +1,109 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { action, computed, observable } from 'mobx'; + +import { useObservableRef, useResource } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { useService } from '@cloudbeaver/core-di'; +import { type ILoadableState, isContainsException } from '@cloudbeaver/core-utils'; + +import { TableViewerStorageService } from './TableViewer/TableViewerStorageService.js'; + +export interface IDataViewerDatabaseDataModel extends ILoadableState { + connectionKey: IConnectionInfoParams | undefined; + tableViewerStorageService: TableViewerStorageService; + tableId?: string; + _exception?: Error[] | Error | null; + _loading: boolean; + _init(): Promise; + init(): Promise; + load(): Promise; +} + +export function useDataViewerModel(connectionKey: IConnectionInfoParams | undefined, init: () => Promise, tableId?: string) { + const tableViewerStorageService = useService(TableViewerStorageService); + const connection = useResource(useDataViewerModel, ConnectionInfoResource, connectionKey ?? null); + + const state = useObservableRef( + () => ({ + _exception: null, + _loading: false, + get exception() { + if (isContainsException(connection.exception)) { + return connection.exception; + } + + return this._exception; + }, + isLoading(): boolean { + return connection.isLoading() || this._loading; + }, + isLoaded(): boolean { + return connection.isLoaded() && this.tableViewerStorageService.get(this.tableId || '') !== undefined; + }, + isError(): boolean { + return isContainsException(this.exception); + }, + async reload() { + if (isContainsException(connection.exception)) { + connection.reload(); + } + + this._init(); + }, + async load() { + if (isContainsException(this.exception)) { + return; + } + + await this._init(); + }, + resetException() { + this._exception = null; + }, + async _init() { + if (this._loading) { + return; + } + + this._loading = true; + + try { + if (!this.connectionKey) { + this._exception = null; + return; + } + + await this.init(); + this._exception = null; + } catch (exception: any) { + this._exception = exception; + } finally { + this._loading = false; + } + }, + }), + { + exception: computed, + _loading: observable.ref, + _exception: observable.ref, + isLoaded: action.bound, + isLoading: action.bound, + isError: action.bound, + reload: action.bound, + }, + { + connectionKey, + tableId, + tableViewerStorageService, + init, + }, + ); + + return state; +} diff --git a/webapp/packages/plugin-data-viewer/tsconfig.json b/webapp/packages/plugin-data-viewer/tsconfig.json index 24b3242a87..0332ac6bba 100644 --- a/webapp/packages/plugin-data-viewer/tsconfig.json +++ b/webapp/packages/plugin-data-viewer/tsconfig.json @@ -1,79 +1,98 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-codemirror6/tsconfig.json" + "path": "../../common-react/@dbeaver/react-tests" }, { - "path": "../plugin-navigation-tabs/tsconfig.json" + "path": "../../common-typescript/@dbeaver/cli" }, { - "path": "../plugin-object-viewer/tsconfig.json" + "path": "../../common-typescript/@dbeaver/js-helpers" }, { - "path": "../core-authentication/tsconfig.json" + "path": "../../common-typescript/@dbeaver/result-set-api" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-browser" }, { - "path": "../core-data-context/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-connections" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-executor/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-navigation-tree/tsconfig.json" + "path": "../core-executor" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-links" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-navigation-tree" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-settings/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-sdk" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-settings" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-view/tsconfig.json" + "path": "../core-utils" }, { - "path": "../tests-runner/tsconfig.json" + "path": "../core-view" + }, + { + "path": "../plugin-codemirror6" + }, + { + "path": "../plugin-datasource-context-switch" + }, + { + "path": "../plugin-navigation-tabs" + }, + { + "path": "../plugin-object-viewer" + }, + { + "path": "../plugin-sql-editor" + }, + { + "path": "../plugin-sql-editor-navigation-tab" } ], "include": [ @@ -85,7 +104,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-datasource-context-switch/package.json b/webapp/packages/plugin-datasource-context-switch/package.json index 3da295a95f..fbfbbb907c 100644 --- a/webapp/packages/plugin-datasource-context-switch/package.json +++ b/webapp/packages/plugin-datasource-context-switch/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-datasource-context-switch", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,36 +11,42 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-navigation-tabs": "~0.1.0", - "@cloudbeaver/plugin-top-app-bar": "~0.1.0", - "@cloudbeaver/core-authentication": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-extensions": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-projects": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-authentication": "workspace:*", + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-extensions": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-navigation-tree": "workspace:*", + "@cloudbeaver/core-projects": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-navigation-tabs": "workspace:*", + "@cloudbeaver/plugin-top-app-bar": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts index 2bfa8e7dd4..48b0175ef8 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts @@ -1,13 +1,15 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { AppAuthService } from '@cloudbeaver/core-authentication'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { compareConnectionsInfo, + ConnectionInfoActiveProjectKey, ConnectionInfoResource, ConnectionsManagerService, ConnectionsSettingsService, @@ -18,19 +20,34 @@ import { import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { EObjectFeature, NodeManagerUtils } from '@cloudbeaver/core-navigation-tree'; +import { ProjectsService } from '@cloudbeaver/core-projects'; import { getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; import { OptionsPanelService } from '@cloudbeaver/core-ui'; -import { DATA_CONTEXT_MENU, MenuBaseItem, menuExtractItems, MenuSeparatorItem, MenuService } from '@cloudbeaver/core-view'; +import { MenuBaseItem, menuExtractItems, MenuSeparatorItem, MenuService } from '@cloudbeaver/core-view'; import { MENU_APP_ACTIONS } from '@cloudbeaver/plugin-top-app-bar'; -import { ConnectionSchemaManagerService } from './ConnectionSchemaManagerService'; -import { ConnectionIcon } from './ConnectionSelector/ConnectionIcon'; -import { ConnectionIconSmall } from './ConnectionSelector/ConnectionIconSmall'; -import type { IConnectionSelectorExtraProps } from './ConnectionSelector/IConnectionSelectorExtraProps'; -import { MENU_CONNECTION_DATA_CONTAINER_SELECTOR } from './MENU_CONNECTION_DATA_CONTAINER_SELECTOR'; -import { MENU_CONNECTION_SELECTOR } from './MENU_CONNECTION_SELECTOR'; +import { ConnectionSchemaManagerService } from './ConnectionSchemaManagerService.js'; +import type { IConnectionSelectorExtraProps } from './ConnectionSelector/IConnectionSelectorExtraProps.js'; +import { MENU_CONNECTION_DATA_CONTAINER_SELECTOR } from './MENU_CONNECTION_DATA_CONTAINER_SELECTOR.js'; +import { MENU_CONNECTION_SELECTOR } from './MENU_CONNECTION_SELECTOR.js'; -@injectable() +const ConnectionIcon = importLazyComponent(() => import('./ConnectionSelector/ConnectionIcon.js').then(module => module.ConnectionIcon)); +const ConnectionIconSmall = importLazyComponent(() => + import('./ConnectionSelector/ConnectionIconSmall.js').then(module => module.ConnectionIconSmall), +); + +@injectable(() => [ + ConnectionInfoResource, + ConnectionSchemaManagerService, + ConnectionsManagerService, + OptionsPanelService, + AppAuthService, + ContainerResource, + MenuService, + ConnectionsSettingsService, + LocalizationService, + ProjectsService, +]) export class ConnectionSchemaManagerBootstrap extends Bootstrap { get connectionSelectorLoading(): boolean { return this.connectionSchemaManagerService.isChangingConnection || this.connectionsManagerService.containerContainers.isLoading(); @@ -46,11 +63,12 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { private readonly menuService: MenuService, private readonly connectionsSettingsService: ConnectionsSettingsService, private readonly localizationService: LocalizationService, + private readonly projectsService: ProjectsService, ) { super(); } - register(): void { + override register(): void { this.addTopAppMenuItems(); this.connectionInfoResource.onDataUpdate.addHandler( @@ -59,7 +77,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { this.menuService.setHandler({ id: 'connection-selector-base', - isApplicable: context => context.hasValue(DATA_CONTEXT_MENU, MENU_CONNECTION_SELECTOR), + menus: [MENU_CONNECTION_SELECTOR], isLoading: () => this.connectionSelectorLoading, isHidden: () => this.isHidden() || !this.appAuthService.authenticated, isDisabled: () => @@ -79,7 +97,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { iconComponent: () => ConnectionIcon, hideIfEmpty: () => false, getExtraProps: () => ({ connectionKey: this.connectionSchemaManagerService.currentConnectionKey, small: true }), - getLoader: (context, menu) => { + getLoader: () => { if (this.isHidden()) { return []; } @@ -87,11 +105,13 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { const activeConnectionKey = this.connectionSchemaManagerService.activeConnectionKey; if (!activeConnectionKey) { - return this.appAuthService.loaders; + return [...this.appAuthService.loaders, getCachedMapResourceLoaderState(this.connectionInfoResource, () => ConnectionInfoActiveProjectKey)]; } return [ ...this.appAuthService.loaders, + ...this.connectionSchemaManagerService.currentObjectLoaders, + getCachedMapResourceLoaderState(this.connectionInfoResource, () => ConnectionInfoActiveProjectKey), getCachedMapResourceLoaderState(this.containerResource, () => ({ ...activeConnectionKey, catalogId: this.connectionSchemaManagerService.activeObjectCatalogId, @@ -105,18 +125,23 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { isApplicable: () => this.connectionsManagerService.hasAnyConnection() && this.connectionSchemaManagerService.isConnectionChangeable, getItems: (context, items) => { items = [...items]; + const userProjectId = this.projectsService.userProject?.id; + const activeProjectId = this.connectionSchemaManagerService.activeProjectId; const connections = this.connectionsManagerService.projectConnections .filter(connection => { + // we want to show connections from active project and user project if ( !this.connectionSchemaManagerService.isProjectChangeable && - this.connectionSchemaManagerService.activeProjectId && - connection.projectId !== this.connectionSchemaManagerService.activeProjectId + activeProjectId && + activeProjectId !== connection.projectId && + activeProjectId !== userProjectId && + connection.projectId !== userProjectId ) { return false; } - return !connection.template; + return true; }) .sort((a, b) => { if (a.connected === b.connected) { @@ -159,7 +184,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { this.menuService.setHandler({ id: 'connection-data-container-selector-base', - isApplicable: context => context.hasValue(DATA_CONTEXT_MENU, MENU_CONNECTION_DATA_CONTAINER_SELECTOR), + menus: [MENU_CONNECTION_DATA_CONTAINER_SELECTOR], isDisabled: () => !this.connectionSchemaManagerService.currentConnection?.connected || this.connectionSelectorLoading || @@ -176,7 +201,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { !this.connectionSchemaManagerService.isObjectSchemaChangeable) || (this.connectionSchemaManagerService.objectContainerList.schemaList.length === 0 && this.connectionSchemaManagerService.objectContainerList.catalogList.length === 0), - getLoader: (context, menu) => { + getLoader: () => { if (this.isHidden()) { return []; } @@ -370,11 +395,9 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { }); } - load(): void {} - private isHidden(): boolean { return ( - this.connectionsSettingsService.settings.getValue('disabled') || + this.connectionsSettingsService.disabled || this.optionsPanelService.active || (!this.connectionSchemaManagerService.isConnectionChangeable && !this.connectionSchemaManagerService.currentConnectionKey) ); @@ -385,9 +408,8 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { menus: [MENU_APP_ACTIONS], getItems: (context, items) => [...items, MENU_CONNECTION_SELECTOR, MENU_CONNECTION_DATA_CONTAINER_SELECTOR], orderItems: (context, items) => { - const extracted = menuExtractItems(items, [MENU_CONNECTION_SELECTOR, MENU_CONNECTION_DATA_CONTAINER_SELECTOR]); - - return [...items, ...extracted]; + items.push(...menuExtractItems(items, [MENU_CONNECTION_SELECTOR, MENU_CONNECTION_DATA_CONTAINER_SELECTOR])); + return items; }, }); } diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts index aa4899d734..0b92e37f50 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,41 +8,46 @@ import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { - Connection, + type Connection, ConnectionInfoResource, ConnectionsManagerService, DBDriverResource, - IConnectionInfoParams, - IConnectionProvider, - IConnectionSetter, - IObjectCatalogProvider, - IObjectCatalogSetter, - IObjectSchemaProvider, - IObjectSchemaSetter, + type IConnectionInfoParams, + type IConnectionProvider, + type IConnectionSetter, + type IExecutionContextProvider, + type IObjectCatalogProvider, + type IObjectCatalogSetter, + type IObjectLoaderProvider, + type IObjectSchemaProvider, + type IObjectSchemaSetter, isConnectionProvider, isConnectionSetter, + isExecutionContextProvider, isObjectCatalogProvider, isObjectCatalogSetter, + isObjectLoaderProvider, isObjectSchemaProvider, isObjectSchemaSetter, - IStructContainers, - ObjectContainer, + type IStructContainers, + type ObjectContainer, serializeConnectionParam, } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ExtensionUtils, IExtension } from '@cloudbeaver/core-extensions'; +import { ExtensionUtils, type IExtension } from '@cloudbeaver/core-extensions'; import { type IDataContextActiveNode, type IObjectNavNodeProvider, isObjectNavNodeProvider } from '@cloudbeaver/core-navigation-tree'; import { - IProjectProvider, - IProjectSetter, - IProjectSetterState, + type IProjectProvider, + type IProjectSetter, + type IProjectSetterState, isProjectProvider, isProjectSetter, isProjectSetterState, } from '@cloudbeaver/core-projects'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; +import type { ILoadableState } from '@cloudbeaver/core-utils'; +import { type ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; export interface IConnectionInfo { name?: string; @@ -58,13 +63,15 @@ interface IActiveItem { getCurrentConnectionId?: IConnectionProvider; getCurrentSchemaId?: IObjectSchemaProvider; getCurrentCatalogId?: IObjectCatalogProvider; + getCurrentExecutionContext?: IExecutionContextProvider; + getCurrentLoader?: IObjectLoaderProvider; changeConnectionId?: IConnectionSetter; changeProjectId?: IProjectSetter; changeCatalogId?: IObjectCatalogSetter; changeSchemaId?: IObjectSchemaSetter; } -@injectable() +@injectable(() => [NavigationTabsService, ConnectionInfoResource, ConnectionsManagerService, DBDriverResource, NotificationService]) export class ConnectionSchemaManagerService { get activeNavNode(): IDataContextActiveNode | null | undefined { if (!this.activeItem?.getCurrentNavNode) { @@ -121,6 +128,22 @@ export class ConnectionSchemaManagerService { return this.activeObjectCatalogId; } + get activeExecutionContext() { + if (!this.activeItem?.getCurrentExecutionContext) { + return; + } + + return this.activeItem.getCurrentExecutionContext(this.activeItem.context); + } + + get currentObjectLoaders(): ILoadableState[] { + if (!this.activeItem?.getCurrentLoader) { + return []; + } + + return this.activeItem.getCurrentLoader(this.activeItem.context); + } + get currentObjectSchemaId(): string | undefined { if (this.pendingSchemaId !== null) { return this.pendingSchemaId; @@ -138,7 +161,7 @@ export class ConnectionSchemaManagerService { return; } - return this.connectionInfo.get(this.currentConnectionKey); + return this.connectionInfoResource.get(this.currentConnectionKey); } get currentObjectCatalog(): ObjectContainer | undefined { @@ -227,7 +250,7 @@ export class ConnectionSchemaManagerService { constructor( private readonly navigationTabsService: NavigationTabsService, - private readonly connectionInfo: ConnectionInfoResource, + private readonly connectionInfoResource: ConnectionInfoResource, private readonly connectionsManagerService: ConnectionsManagerService, private readonly dbDriverResource: DBDriverResource, private readonly notificationService: NotificationService, @@ -260,6 +283,7 @@ export class ConnectionSchemaManagerService { currentObjectCatalogId: computed, activeObjectCatalogId: computed, currentObjectSchemaId: computed, + currentObjectLoaders: computed, isConnectionChangeable: computed, isObjectCatalogChangeable: computed, isObjectSchemaChangeable: computed, @@ -398,7 +422,7 @@ export class ConnectionSchemaManagerService { return; } - const connection = this.connectionInfo.get(key); + const connection = this.connectionInfoResource.get(key); if (!connection) { console.warn(`Connection Schema Manager: connection (${serializeConnectionParam(key)}) not exists`); @@ -442,6 +466,12 @@ export class ConnectionSchemaManagerService { .on(isObjectSchemaProvider, extension => { item.getCurrentSchemaId = extension; }) + .on(isExecutionContextProvider, extension => { + item.getCurrentExecutionContext = extension; + }) + .on(isObjectLoaderProvider, extension => { + item.getCurrentLoader = extension; + }) .on(isProjectSetter, extension => { item.changeProjectId = extension; diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIcon.module.css b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIcon.module.css new file mode 100644 index 0000000000..6b61e8851a --- /dev/null +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIcon.module.css @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.connectionIcon { + position: relative; + display: flex; +} +.connectionImageWithMask { + border-radius: var(--theme-form-element-radius); + + &.small { + box-sizing: border-box; + } +} diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIcon.tsx b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIcon.tsx index 6ebd824d23..8926de2203 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIcon.tsx +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIcon.tsx @@ -1,46 +1,31 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled, { css, use } from 'reshadow'; import { ConnectionImageWithMask, ConnectionImageWithMaskSvgStyles, + getComputed, s, SContext, - StyleRegistry, + type StyleRegistry, useResource, - useStyles, + useS, } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource, DBDriverResource } from '@cloudbeaver/core-connections'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import type { ComponentStyle } from '@cloudbeaver/core-theming'; -import { default as ConnectionImageWithMaskSvgBackgroundStyles } from './ConnectionImageWithMask.m.css'; -import type { IConnectionSelectorExtraProps } from './IConnectionSelectorExtraProps'; +import styles from './ConnectionIcon.module.css'; +import ConnectionImageWithMaskSvgBackgroundStyles from './ConnectionImageWithMask.module.css'; +import type { IConnectionSelectorExtraProps } from './IConnectionSelectorExtraProps.js'; -const connectionIconStyle = css` - icon { - position: relative; - display: flex; - - & ConnectionImageWithMask { - border-radius: var(--theme-form-element-radius); - - &[|small] { - box-sizing: border-box; - } - } - } -`; - -interface Props extends IConnectionSelectorExtraProps { - style?: ComponentStyle; +export interface ConnectionIconProps extends IConnectionSelectorExtraProps { + size?: number; className?: string; } @@ -54,35 +39,40 @@ const registry: StyleRegistry = [ ], ]; -export const ConnectionIcon: React.FC = observer(function ConnectionIcon({ connectionKey, small = true, style, className }) { - const styles = useStyles(style, connectionIconStyle); - +export const ConnectionIcon = observer(function ConnectionIcon({ connectionKey, size = 24, small = true, className }) { const connection = useResource(ConnectionIcon, ConnectionInfoResource, connectionKey ?? null); - const drivers = useResource(ConnectionIcon, DBDriverResource, CachedMapAllKey); + const style = useS(styles, ConnectionImageWithMaskSvgBackgroundStyles); if (!connection.data?.driverId) { return null; } - const driver = drivers.resource.get(connection.data.driverId); + const connected = getComputed(() => connection.data?.connected ?? false); + const driverIcon = getComputed(() => { + if (!connection.data?.driverId) { + return null; + } + + return drivers.resource.get(connection.data.driverId)?.icon; + }); - if (!driver?.icon) { + if (!driverIcon) { return null; } - return styled(styles)( - + return ( +
- , +
); }); diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIconSmall.tsx b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIconSmall.tsx index 2e4c6e5a91..1edea06d53 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIconSmall.tsx +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIconSmall.tsx @@ -1,88 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { observer } from 'mobx-react-lite'; -import styled, { css, use } from 'reshadow'; +import { ConnectionIcon, type ConnectionIconProps } from './ConnectionIcon.js'; -import { - ConnectionImageWithMask, - ConnectionImageWithMaskSvgStyles, - s, - SContext, - StyleRegistry, - useResource, - useStyles, -} from '@cloudbeaver/core-blocks'; -import { ConnectionInfoResource, DBDriverResource } from '@cloudbeaver/core-connections'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import type { ComponentStyle } from '@cloudbeaver/core-theming'; - -import { default as ConnectionImageWithMaskSvgBackgroundStyles } from './ConnectionImageWithMask.m.css'; -import type { IConnectionSelectorExtraProps } from './IConnectionSelectorExtraProps'; - -const connectionIconStyle = css` - icon { - position: relative; - display: flex; - - & ConnectionImageWithMask { - border-radius: var(--theme-form-element-radius); - - &[|small] { - box-sizing: border-box; - } - } - } -`; - -interface Props extends IConnectionSelectorExtraProps { - style?: ComponentStyle; - className?: string; -} - -const registry: StyleRegistry = [ - [ - ConnectionImageWithMaskSvgStyles, - { - mode: 'append', - styles: [ConnectionImageWithMaskSvgBackgroundStyles], - }, - ], -]; - -export const ConnectionIconSmall: React.FC = observer(function ConnectionIconSmall({ connectionKey, small = true, style, className }) { - const styles = useStyles(style, connectionIconStyle); - - const connection = useResource(ConnectionIconSmall, ConnectionInfoResource, connectionKey ?? null); - - const drivers = useResource(ConnectionIconSmall, DBDriverResource, CachedMapAllKey); - - if (!connection.data?.driverId) { - return null; - } - - const driver = drivers.resource.get(connection.data.driverId); - - if (!driver?.icon) { - return null; - } - - return styled(styles)( - - - - - , - ); -}); +export const ConnectionIconSmall: React.FC = function ConnectionIconSmall(props) { + return ; +}; diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionImageWithMask.m.css b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionImageWithMask.m.css deleted file mode 100644 index 8f5c63106a..0000000000 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionImageWithMask.m.css +++ /dev/null @@ -1,3 +0,0 @@ -.connectionIcon .background { - fill: #fff; -} diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionImageWithMask.module.css b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionImageWithMask.module.css new file mode 100644 index 0000000000..9f76a7ceaf --- /dev/null +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionImageWithMask.module.css @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.connectionIcon .background { + fill: #fff; +} diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/IConnectionSelectorExtraProps.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/IConnectionSelectorExtraProps.ts index 307694acb7..6e358f9ea9 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/IConnectionSelectorExtraProps.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/IConnectionSelectorExtraProps.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/MENU_CONNECTION_DATA_CONTAINER_SELECTOR.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/MENU_CONNECTION_DATA_CONTAINER_SELECTOR.ts index e6275028c6..744ed5db07 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/MENU_CONNECTION_DATA_CONTAINER_SELECTOR.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/MENU_CONNECTION_DATA_CONTAINER_SELECTOR.ts @@ -1,10 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const MENU_CONNECTION_DATA_CONTAINER_SELECTOR = createMenu('connection-data-container-selector', 'Connection data container selector'); +export const MENU_CONNECTION_DATA_CONTAINER_SELECTOR = createMenu('connection-data-container-selector', { + label: 'Connection data container selector', +}); diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/MENU_CONNECTION_SELECTOR.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/MENU_CONNECTION_SELECTOR.ts index e7a6479ca5..0161383de7 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/MENU_CONNECTION_SELECTOR.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/MENU_CONNECTION_SELECTOR.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const MENU_CONNECTION_SELECTOR = createMenu('connection-selector', 'Connection selector'); +export const MENU_CONNECTION_SELECTOR = createMenu('connection-selector', { label: 'Connection selector' }); diff --git a/webapp/packages/plugin-datasource-context-switch/src/LocaleService.ts b/webapp/packages/plugin-datasource-context-switch/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/LocaleService.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-datasource-context-switch/src/PluginBootstrap.ts b/webapp/packages/plugin-datasource-context-switch/src/PluginBootstrap.ts index 5cb98d6fdd..09687f9836 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/PluginBootstrap.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -12,8 +12,4 @@ export class PluginBootstrap extends Bootstrap { constructor() { super(); } - - register(): void | Promise {} - - load(): void | Promise {} } diff --git a/webapp/packages/plugin-datasource-context-switch/src/index.ts b/webapp/packages/plugin-datasource-context-switch/src/index.ts index ed176fc2aa..862f3209c7 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/index.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/index.ts @@ -1,4 +1,13 @@ -export * from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 * from './ConnectionSchemaManager/ConnectionSchemaManagerBootstrap'; -export * from './ConnectionSchemaManager/ConnectionSchemaManagerService'; +import './module.js'; +export * from './manifest.js'; + +export * from './ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.js'; +export * from './ConnectionSchemaManager/ConnectionSchemaManagerService.js'; diff --git a/webapp/packages/plugin-datasource-context-switch/src/locales/fr.ts b/webapp/packages/plugin-datasource-context-switch/src/locales/fr.ts new file mode 100644 index 0000000000..57dcde6a75 --- /dev/null +++ b/webapp/packages/plugin-datasource-context-switch/src/locales/fr.ts @@ -0,0 +1,8 @@ +export default [ + ['plugin_datasource_context_switch_select_connection', 'Sélectionner la connexion'], + ['plugin_datasource_context_switch_select_container', 'Sélectionner le conteneur'], + [ + 'plugin_datasource_context_switch_select_container_tooltip', + 'Les modifications seront appliquées à tous les scripts actifs associés à une connexion "{arg:name}"', + ], +]; diff --git a/webapp/packages/plugin-datasource-context-switch/src/locales/vi.ts b/webapp/packages/plugin-datasource-context-switch/src/locales/vi.ts new file mode 100644 index 0000000000..711b1a8390 --- /dev/null +++ b/webapp/packages/plugin-datasource-context-switch/src/locales/vi.ts @@ -0,0 +1,8 @@ +export default [ + ['plugin_datasource_context_switch_select_connection', 'Chọn kết nối'], + ['plugin_datasource_context_switch_select_container', 'Chọn container'], + [ + 'plugin_datasource_context_switch_select_container_tooltip', + 'Các thay đổi sẽ được áp dụng cho tất cả kịch bản đang hoạt động liên quan đến kết nối "{arg:name}"', + ], +]; diff --git a/webapp/packages/plugin-datasource-context-switch/src/locales/zh.ts b/webapp/packages/plugin-datasource-context-switch/src/locales/zh.ts index 9fce7aa852..83d333d1ab 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/locales/zh.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/locales/zh.ts @@ -1,8 +1,5 @@ export default [ - ['plugin_datasource_context_switch_select_connection', 'Select connection'], - ['plugin_datasource_context_switch_select_container', 'Select container'], - [ - 'plugin_datasource_context_switch_select_container_tooltip', - 'The changes will be applied to all active scripts associated with a "{arg:name}" connection', - ], + ['plugin_datasource_context_switch_select_connection', '选择数据库连接'], + ['plugin_datasource_context_switch_select_container', '选择库/目录'], + ['plugin_datasource_context_switch_select_container_tooltip', '这些更改将应用于与 "{arg:name}" 连接关联的所有活动脚本'], ]; diff --git a/webapp/packages/plugin-datasource-context-switch/src/manifest.ts b/webapp/packages/plugin-datasource-context-switch/src/manifest.ts index 9ce76d9ff2..a356a9641d 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/manifest.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/manifest.ts @@ -1,21 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { ConnectionSchemaManagerBootstrap } from './ConnectionSchemaManager/ConnectionSchemaManagerBootstrap'; -import { ConnectionSchemaManagerService } from './ConnectionSchemaManager/ConnectionSchemaManagerService'; -import { LocaleService } from './LocaleService'; -import { PluginBootstrap } from './PluginBootstrap'; - export const datasourceContextSwitchPluginManifest: PluginManifest = { info: { name: 'Datasource context switch plugin', }, - - providers: [PluginBootstrap, ConnectionSchemaManagerService, ConnectionSchemaManagerBootstrap, LocaleService], }; diff --git a/webapp/packages/plugin-datasource-context-switch/src/module.ts b/webapp/packages/plugin-datasource-context-switch/src/module.ts new file mode 100644 index 0000000000..2023b82ebe --- /dev/null +++ b/webapp/packages/plugin-datasource-context-switch/src/module.ts @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { PluginBootstrap } from './PluginBootstrap.js'; +import { LocaleService } from './LocaleService.js'; +import { ConnectionSchemaManagerService } from './ConnectionSchemaManager/ConnectionSchemaManagerService.js'; +import { ConnectionSchemaManagerBootstrap } from './ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-datasource-context-switch', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, proxy(ConnectionSchemaManagerBootstrap)) + .addSingleton(Bootstrap, PluginBootstrap) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(ConnectionSchemaManagerBootstrap) + .addSingleton(ConnectionSchemaManagerService); + }, +}); diff --git a/webapp/packages/plugin-datasource-context-switch/tsconfig.json b/webapp/packages/plugin-datasource-context-switch/tsconfig.json index 3f02b0c02c..c1bd8eaa4d 100644 --- a/webapp/packages/plugin-datasource-context-switch/tsconfig.json +++ b/webapp/packages/plugin-datasource-context-switch/tsconfig.json @@ -1,55 +1,59 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-navigation-tabs/tsconfig.json" + "path": "../core-authentication" }, { - "path": "../plugin-top-app-bar/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-authentication/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-connections" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-extensions" }, { - "path": "../core-extensions/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-navigation-tree" }, { - "path": "../core-navigation-tree/tsconfig.json" + "path": "../core-projects" }, { - "path": "../core-projects/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-utils" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-view" }, { - "path": "../core-view/tsconfig.json" + "path": "../plugin-navigation-tabs" + }, + { + "path": "../plugin-top-app-bar" } ], "include": [ @@ -61,7 +65,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-datasource-transaction-manager/.gitignore b/webapp/packages/plugin-datasource-transaction-manager/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-datasource-transaction-manager/package.json b/webapp/packages/plugin-datasource-transaction-manager/package.json new file mode 100644 index 0000000000..b0d7e7e83d --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/package.json @@ -0,0 +1,58 @@ +{ + "name": "@cloudbeaver/plugin-datasource-transaction-manager", + "type": "module", + "sideEffects": [ + "./lib/module.js", + "./lib/index.js", + "src/**/*.css", + "src/**/*.scss", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "exports": { + ".": "./lib/index.js" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf --glob lib", + "lint": "eslint ./src/ --ext .ts,.tsx", + "validate-dependencies": "core-cli-validate-dependencies" + }, + "dependencies": { + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-settings": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-codemirror6": "workspace:*", + "@cloudbeaver/plugin-data-grid": "workspace:*", + "@cloudbeaver/plugin-datasource-context-switch": "workspace:*", + "@cloudbeaver/plugin-sql-editor-new": "workspace:*", + "@cloudbeaver/plugin-tools-panel": "workspace:*", + "@cloudbeaver/plugin-top-app-bar": "workspace:*", + "@dbeaver/js-helpers": "workspace:^", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" + }, + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit.svg new file mode 100644 index 0000000000..e640e089ef --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_m.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_m.svg new file mode 100644 index 0000000000..f07ddff927 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_m.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto.svg new file mode 100644 index 0000000000..cfa19cd5a2 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_m.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_m.svg new file mode 100644 index 0000000000..812bca94d1 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_m.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_sm.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_sm.svg new file mode 100644 index 0000000000..fbc45eba62 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_sm.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual.svg new file mode 100644 index 0000000000..62920cce76 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_m.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_m.svg new file mode 100644 index 0000000000..82ca26326a --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_m.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_sm.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_sm.svg new file mode 100644 index 0000000000..f23fd9fbe5 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_sm.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_sm.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_sm.svg new file mode 100644 index 0000000000..6ef412d43f --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_sm.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback.svg new file mode 100644 index 0000000000..6932be5834 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_m.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_m.svg new file mode 100644 index 0000000000..c469264a9a --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_m.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_sm.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_sm.svg new file mode 100644 index 0000000000..f206366047 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_sm.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/LocaleService.ts b/webapp/packages/plugin-datasource-transaction-manager/src/LocaleService.ts new file mode 100644 index 0000000000..6d1a499028 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/LocaleService.ts @@ -0,0 +1,37 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable(() => [LocalizationService]) +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + override register(): void { + this.localizationService.addProvider(this.provider.bind(this)); + } + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru.js')).default; + case 'it': + return (await import('./locales/it.js')).default; + case 'zh': + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; + default: + return (await import('./locales/en.js')).default; + } + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TRANSACTION_INFO_PARAM_SCHEMA.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TRANSACTION_INFO_PARAM_SCHEMA.ts new file mode 100644 index 0000000000..b4e0b04890 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TRANSACTION_INFO_PARAM_SCHEMA.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { schema } from '@cloudbeaver/core-utils'; + +export const TRANSACTION_INFO_PARAM_SCHEMA = schema + .object({ + connectionId: schema.string(), + projectId: schema.string(), + contextId: schema.string(), + }) + .required() + .strict(); + +export type ITransactionInfoParam = schema.infer; + +export function createTransactionInfoParam(connectionId: string, projectId: string, contextId: string): ITransactionInfoParam { + return { + connectionId, + projectId, + contextId, + }; +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.module.css b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.module.css new file mode 100644 index 0000000000..3408475ccf --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.module.css @@ -0,0 +1,38 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.container { + cursor: pointer; + padding: 0 12px; + position: relative; + + &:after { + position: absolute; + background: #236ea0; + height: 32px; + width: 1px; + top: 8px; + right: -1px; + opacity: 1; + content: ''; + } + + &:hover { + background: #338ecc; + } +} + +.count { + border: 1px solid var(--theme-on-primary); + border-radius: var(--theme-form-element-radius); + height: 26px; + width: 50px; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.tsx new file mode 100644 index 0000000000..af59c8222d --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.tsx @@ -0,0 +1,49 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Container, s, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import type { ICustomMenuItemComponent } from '@cloudbeaver/core-view'; + +import { TransactionManagerService } from '../TransactionManagerService.js'; +import { createTransactionInfoParam } from './TRANSACTION_INFO_PARAM_SCHEMA.js'; +import classes from './TransactionInfoAction.module.css'; +import { TransactionLogCountResource } from './TransactionLogCountResource.js'; + +export const TransactionInfoAction: ICustomMenuItemComponent = observer(function TransactionInfoAction(props) { + const styles = useS(classes); + const translate = useTranslate(); + const transactionManagerService = useService(TransactionManagerService); + const transaction = transactionManagerService.getActiveContextTransaction(); + const context = transaction?.context; + const key = context ? createTransactionInfoParam(context.connectionId, context.projectId, context.id) : null; + + const transactionLogCountResource = useResource(TransactionInfoAction, TransactionLogCountResource, key); + const count = + transactionLogCountResource.data === 0 ? translate('plugin_datasource_transaction_manager_logs_counter_none') : transactionLogCountResource.data; + + let title: string = translate('plugin_datasource_transaction_manager_logs_tooltip'); + + if (transactionLogCountResource.data) { + title = `${title}\n${translate('plugin_datasource_transaction_manager_logs_tooltip_count', undefined, { count })}`; + } + + return ( + props.item.events?.onSelect?.(props.context)} + > + {count} + + ); +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountEventHandler.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountEventHandler.ts new file mode 100644 index 0000000000..790a7aef83 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountEventHandler.ts @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { type ISessionEvent, type SessionEventId, SessionEventSource, SessionEventTopic, TopicEventHandler } from '@cloudbeaver/core-root'; +import type { WsTransactionalCountEvent } from '@cloudbeaver/core-sdk'; + +export type IWsTransactionCountEvent = WsTransactionalCountEvent; + +type TransactionCountEvent = IWsTransactionCountEvent; + +@injectable(() => [SessionEventSource]) +export class TransactionLogCountEventHandler extends TopicEventHandler { + constructor(sessionEventSource: SessionEventSource) { + super(SessionEventTopic.CbTransaction, sessionEventSource); + } + + map(event: any): TransactionCountEvent { + return event; + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountResource.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountResource.ts new file mode 100644 index 0000000000..9b45d25636 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountResource.ts @@ -0,0 +1,63 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { toJS } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { CachedMapResource } from '@cloudbeaver/core-resource'; +import { ServerEventId } from '@cloudbeaver/core-root'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; +import { schemaValidationError } from '@cloudbeaver/core-utils'; + +import { createTransactionInfoParam, type ITransactionInfoParam, TRANSACTION_INFO_PARAM_SCHEMA } from './TRANSACTION_INFO_PARAM_SCHEMA.js'; +import { type IWsTransactionCountEvent, TransactionLogCountEventHandler } from './TransactionLogCountEventHandler.js'; + +@injectable(() => [GraphQLService, TransactionLogCountEventHandler]) +export class TransactionLogCountResource extends CachedMapResource { + constructor( + private readonly graphQLService: GraphQLService, + transactionLogCountEventHandler: TransactionLogCountEventHandler, + ) { + super(); + + transactionLogCountEventHandler.onEvent( + ServerEventId.CbTransactionCount, + async data => { + const key = createTransactionInfoParam(data.connectionId, data.projectId, data.contextId); + this.set(key, data.transactionalCount); + }, + undefined, + this, + ); + } + + protected async loader(key: ITransactionInfoParam) { + const { info } = await this.graphQLService.sdk.getTransactionCount({ + connectionId: key.connectionId, + projectId: key.projectId, + contextId: key.contextId, + }); + + this.set(key, info.count); + + return this.data; + } + + override isKeyEqual(key: ITransactionInfoParam, secondKey: ITransactionInfoParam): boolean { + return key.connectionId === secondKey.connectionId && key.projectId === secondKey.projectId; + } + + protected override validateKey(key: ITransactionInfoParam): boolean { + const parse = TRANSACTION_INFO_PARAM_SCHEMA.safeParse(toJS(key)); + + if (!parse.success) { + this.logger.warn(`Invalid resource key ${(schemaValidationError(parse.error).toString(), { prefix: null })}`); + } + + return parse.success; + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogDialog.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogDialog.tsx new file mode 100644 index 0000000000..20f39355ee --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogDialog.tsx @@ -0,0 +1,80 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { + Button, + CommonDialogBody, + CommonDialogFooter, + CommonDialogHeader, + CommonDialogWrapper, + Container, + Flex, + useAutoLoad, + useResource, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { type ConnectionExecutionContext, ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; +import type { DialogComponent } from '@cloudbeaver/core-dialogs'; + +import { TransactionLogTable } from './TransactionLogTable/TransactionLogTable.js'; +import { useTransactionLog } from './useTransactionLog.js'; + +interface IPayload { + transaction: ConnectionExecutionContext; + onCommit: () => void; + onRollback: () => void; +} + +export const TransactionLogDialog: DialogComponent = observer(function TransactionLogDialog(props) { + const translate = useTranslate(); + const context = props.payload.transaction.context; + const connectionParam = context ? createConnectionParam(context.projectId, context.connectionId) : null; + + const state = useTransactionLog(props.payload); + const connectionInfoResource = useResource(useTransactionLog, ConnectionInfoResource, connectionParam); + let title: string = translate('plugin_datasource_transaction_manager_logs'); + + if (connectionInfoResource.data?.name) { + title = `${title} (${connectionInfoResource.data.name})`; + } + + useAutoLoad(TransactionLogDialog, state); + + function handleRollback() { + props.payload.onRollback(); + props.resolveDialog(); + } + + function handleCommit() { + props.payload.onCommit(); + props.resolveDialog(); + } + + return ( + + + + + + + + + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.module.css b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.module.css new file mode 100644 index 0000000000..86cf517ef8 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.cell { + text-overflow: ellipsis; + overflow: hidden; +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.tsx new file mode 100644 index 0000000000..2e4e5220a8 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.tsx @@ -0,0 +1,36 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +import type { TransactionLogInfoItem } from '@cloudbeaver/core-sdk'; +import { Link } from '@cloudbeaver/core-blocks'; + +import { QueryDetailsDialog } from './QueryDetailsDialog.js'; + +interface Props { + row: TransactionLogInfoItem; +} + +export const QueryCell = observer(function QueryCell({ row }) { + const commonDialogService = useService(CommonDialogService); + const value = row.queryString; + + async function openDetails() { + await commonDialogService.open(QueryDetailsDialog, { + text: value, + }); + } + + return ( + + {value} + + ); +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryDetailsDialog.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryDetailsDialog.tsx new file mode 100644 index 0000000000..9034c952df --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryDetailsDialog.tsx @@ -0,0 +1,38 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Button, CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, useTranslate } from '@cloudbeaver/core-blocks'; +import type { DialogComponent } from '@cloudbeaver/core-dialogs'; +import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; +import { SQLCodeEditorLoader, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-new'; + +interface IPayload { + text: string; +} + +export const QueryDetailsDialog: DialogComponent = observer(function QueryDetailsDialog(props) { + const translate = useTranslate(); + const sqlDialect = useSqlDialectExtension(undefined); + const extensions = useCodemirrorExtensions(); + extensions.set(...sqlDialect); + + return ( + + + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TimeCell.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TimeCell.tsx new file mode 100644 index 0000000000..d8e6fef6d0 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TimeCell.tsx @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import type { TransactionLogInfoItem } from '@cloudbeaver/core-sdk'; +import { isSameDay } from '@cloudbeaver/core-utils'; + +interface Props { + row: TransactionLogInfoItem; +} + +export const TimeCell = observer(function TimeCell({ row }) { + const date = new Date(row.time); + const fullTime = date.toLocaleString(); + const displayTime = isSameDay(date, new Date()) ? date.toLocaleTimeString() : fullTime; + + return displayTime; +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.module.css b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.module.css new file mode 100644 index 0000000000..5ef47b9de6 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.container { + composes: theme-typography--caption from global; + height: 100%; +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.tsx new file mode 100644 index 0000000000..432f813c78 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.tsx @@ -0,0 +1,98 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { reaction } from 'mobx'; +import { observer } from 'mobx-react-lite'; + +import { s, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import type { TransactionLogInfoItem } from '@cloudbeaver/core-sdk'; +import { DataGrid, useCreateGridReactiveValue } from '@cloudbeaver/plugin-data-grid'; + +import { QueryCell } from './QueryCell.js'; +import { TimeCell } from './TimeCell.js'; +import classes from './TransactionLogTable.module.css'; + +interface Props { + log: TransactionLogInfoItem[]; +} + +const QUERY_COLUMN_WIDTH = 250; + +export const TransactionLogTable = observer(function TransactionLogTable({ log }) { + const styles = useS(classes); + const translate = useTranslate(); + + const columnCount = useCreateGridReactiveValue(() => 6, null, []); + const rowCount = useCreateGridReactiveValue( + () => log.length, + onValueChange => reaction(() => log.length, onValueChange), + [log], + ); + + function getCell(rowIdx: number, colIdx: number) { + switch (colIdx) { + case 0: + return ; + case 1: + return log![rowIdx]?.type ?? ''; + case 2: + return ; + case 3: + return String(log![rowIdx]?.durationMs ?? ''); + case 4: + return String(log![rowIdx]?.rows ?? ''); + case 5: + return log![rowIdx]?.result ?? ''; + } + + return ''; + } + const cell = useCreateGridReactiveValue(getCell, (onValueChange, rowIdx, colIdx) => reaction(() => getCell(rowIdx, colIdx), onValueChange), [log]); + + function getHeaderText(colIdx: number) { + switch (colIdx) { + case 0: + return translate('plugin_datasource_transaction_manager_logs_table_column_time'); + case 1: + return translate('plugin_datasource_transaction_manager_logs_table_column_type'); + case 2: + return translate('plugin_datasource_transaction_manager_logs_table_column_text'); + case 3: + return translate('plugin_datasource_transaction_manager_logs_table_column_duration'); + case 4: + return translate('plugin_datasource_transaction_manager_logs_table_column_rows'); + case 5: + return translate('plugin_datasource_transaction_manager_logs_table_column_result'); + } + + return ''; + } + + const headerText = useCreateGridReactiveValue(getHeaderText, (onValueChange, colIdx) => reaction(() => getHeaderText(colIdx), onValueChange), []); + + function getHeaderWidth(colIdx: number) { + switch (colIdx) { + case 2: + return QUERY_COLUMN_WIDTH; + default: + return 'auto'; + } + } + + return ( +
+ 30} + rowCount={rowCount} + /> +
+ ); +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/useTransactionLog.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/useTransactionLog.tsx new file mode 100644 index 0000000000..1d75c1c0c8 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/useTransactionLog.tsx @@ -0,0 +1,60 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import type { ConnectionExecutionContext } from '@cloudbeaver/core-connections'; +import type { TransactionLogInfoItem } from '@cloudbeaver/core-sdk'; +import type { ILoadableState } from '@cloudbeaver/core-utils'; + +interface Payload { + transaction: ConnectionExecutionContext; +} + +interface State extends ILoadableState { + log: TransactionLogInfoItem[] | null; + exception: Error | null; + promise: Promise | null; + payload: Payload; +} + +export function useTransactionLog(payload: Payload) { + const state = useObservableRef( + () => ({ + log: null, + exception: null, + promise: null, + isLoaded() { + return this.log !== null; + }, + isError() { + return this.exception !== null; + }, + isLoading() { + return this.promise !== null; + }, + async load() { + try { + this.exception = null; + + this.promise = payload.transaction.getLog(); + const log = await this.promise; + this.log = log; + } catch (exception: any) { + this.exception = exception; + } finally { + this.promise = null; + } + }, + }), + { log: observable.ref, promise: observable.ref, exception: observable.ref, payload: observable.ref }, + { payload }, + ); + + return state; +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerBootstrap.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerBootstrap.ts new file mode 100644 index 0000000000..a94590bd8c --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerBootstrap.ts @@ -0,0 +1,295 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks'; +import { + ConnectionExecutionContext, + ConnectionExecutionContextResource, + ConnectionExecutionContextService, + ConnectionInfoResource, + ConnectionsManagerService, + createConnectionParam, + type IConnectionExecutionContextUpdateTaskInfo, + type IConnectionExecutorData, + isConnectionInfoParamEqual, +} from '@cloudbeaver/core-connections'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { OptionsPanelService } from '@cloudbeaver/core-ui'; +import { isNotNullDefined } from '@dbeaver/js-helpers'; +import { ActionService, MenuCustomItem, menuItemsPlaceAfter, MenuService } from '@cloudbeaver/core-view'; +import { ConnectionSchemaManagerService } from '@cloudbeaver/plugin-datasource-context-switch'; +import { MENU_APP_ACTIONS } from '@cloudbeaver/plugin-top-app-bar'; +import { MENU_TOOLS } from '@cloudbeaver/plugin-tools-panel'; + +import { ACTION_DATASOURCE_TRANSACTION_COMMIT } from './actions/ACTION_DATASOURCE_TRANSACTION_COMMIT.js'; +import { ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE } from './actions/ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE.js'; +import { ACTION_DATASOURCE_TRANSACTION_ROLLBACK } from './actions/ACTION_DATASOURCE_TRANSACTION_ROLLBACK.js'; +import { createTransactionInfoParam } from './TransactionLog/TRANSACTION_INFO_PARAM_SCHEMA.js'; +import { TransactionLogCountResource } from './TransactionLog/TransactionLogCountResource.js'; +import { TransactionManagerSettingsService } from './TransactionManagerSettingsService.js'; + +const TransactionInfoAction = importLazyComponent(() => + import('./TransactionLog/TransactionInfoAction.js').then(module => module.TransactionInfoAction), +); + +const TransactionLogDialog = importLazyComponent(() => + import('./TransactionLog/TransactionLogDialog.js').then(module => module.TransactionLogDialog), +); + +@injectable(() => [ + MenuService, + ActionService, + ConnectionSchemaManagerService, + ConnectionExecutionContextService, + ConnectionExecutionContextResource, + ConnectionInfoResource, + ConnectionsManagerService, + OptionsPanelService, + NotificationService, + CommonDialogService, + LocalizationService, + TransactionManagerSettingsService, + TransactionLogCountResource, +]) +export class TransactionManagerBootstrap extends Bootstrap { + constructor( + private readonly menuService: MenuService, + private readonly actionService: ActionService, + private readonly connectionSchemaManagerService: ConnectionSchemaManagerService, + private readonly connectionExecutionContextService: ConnectionExecutionContextService, + private readonly connectionExecutionContextResource: ConnectionExecutionContextResource, + private readonly connectionInfoResource: ConnectionInfoResource, + private readonly connectionsManagerService: ConnectionsManagerService, + private readonly optionsPanelService: OptionsPanelService, + private readonly notificationService: NotificationService, + private readonly commonDialogService: CommonDialogService, + private readonly localizationService: LocalizationService, + private readonly transactionManagerSettingsService: TransactionManagerSettingsService, + private readonly transactionLogCountResource: TransactionLogCountResource, + ) { + super(); + } + + override register() { + this.connectionsManagerService.onDisconnect.addHandler(this.disconnectHandler.bind(this)); + + const TRANSACTION_INFO_ITEM = new MenuCustomItem( + { + id: 'transaction-info', + getComponent: () => TransactionInfoAction, + }, + { + onSelect: async () => { + const transaction = this.getContextTransaction(); + + if (transaction) { + await this.commonDialogService.open(TransactionLogDialog, { + transaction, + onCommit: () => this.commit(transaction), + onRollback: () => this.rollback(transaction), + }); + } + }, + }, + ); + + this.menuService.addCreator({ + menus: [MENU_APP_ACTIONS], + isApplicable: () => { + const transaction = this.getContextTransaction(); + + return ( + !this.transactionManagerSettingsService.disabled && + !this.optionsPanelService.active && + this.connectionSchemaManagerService.currentConnection?.connected === true && + !!transaction?.context && + isNotNullDefined(transaction.autoCommit) + ); + }, + getItems: (_, items) => { + const transaction = this.getContextTransaction(); + + const result = [ + ...items, + ACTION_DATASOURCE_TRANSACTION_COMMIT, + ACTION_DATASOURCE_TRANSACTION_ROLLBACK, + ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE, + ]; + + if (transaction && transaction.autoCommit === false) { + result.push(TRANSACTION_INFO_ITEM); + } + + return result; + }, + orderItems: (context, items) => { + menuItemsPlaceAfter( + items, + [ + ACTION_DATASOURCE_TRANSACTION_COMMIT, + ACTION_DATASOURCE_TRANSACTION_ROLLBACK, + ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE, + TRANSACTION_INFO_ITEM, + ], + MENU_TOOLS, + ); + + return items; + }, + }); + + this.actionService.addHandler({ + id: 'commit-mode-base', + actions: [ACTION_DATASOURCE_TRANSACTION_COMMIT, ACTION_DATASOURCE_TRANSACTION_ROLLBACK, ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE], + isLabelVisible: (_, action) => action === ACTION_DATASOURCE_TRANSACTION_COMMIT || action === ACTION_DATASOURCE_TRANSACTION_ROLLBACK, + getActionInfo: (_, action) => { + const transaction = this.getContextTransaction(); + + if (!transaction) { + return action.info; + } + + if (action === ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE) { + const auto = transaction.autoCommit; + const icon = `/icons/commit_mode_${auto ? 'auto' : 'manual'}_m.svg`; + const label = `plugin_datasource_transaction_manager_commit_mode_switch_to_${auto ? 'manual' : 'auto'}`; + + return { ...action.info, icon, label, tooltip: label }; + } + + return action.info; + }, + isDisabled: () => { + const transaction = this.getContextTransaction(); + return transaction?.executing === true; + }, + isHidden: (_, action) => { + const transaction = this.getContextTransaction(); + + if (!transaction) { + return true; + } + + if (action === ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE) { + return !this.transactionManagerSettingsService.allowCommitModeSwitch; + } + + if (action === ACTION_DATASOURCE_TRANSACTION_COMMIT || action === ACTION_DATASOURCE_TRANSACTION_ROLLBACK) { + return transaction.autoCommit === true; + } + + return false; + }, + handler: async (_, action) => { + const transaction = this.getContextTransaction(); + + if (!transaction) { + return; + } + + switch (action) { + case ACTION_DATASOURCE_TRANSACTION_COMMIT: { + await this.commit(transaction); + break; + } + case ACTION_DATASOURCE_TRANSACTION_ROLLBACK: { + await this.rollback(transaction); + break; + } + case ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE: + try { + await transaction.setAutoCommit(!transaction.autoCommit); + await this.connectionExecutionContextResource.refresh(); + + const context = transaction.context; + + if (transaction.autoCommit === true && context) { + this.transactionLogCountResource.markOutdated(createTransactionInfoParam(context.connectionId, context.projectId, context.id)); + } + } catch (exception: any) { + this.notificationService.logException(exception, 'plugin_datasource_transaction_manager_commit_mode_fail'); + } + + break; + } + }, + }); + } + + private showTransactionResult(transaction: ConnectionExecutionContext, info: IConnectionExecutionContextUpdateTaskInfo) { + if (!transaction.context) { + return; + } + + const connectionParam = createConnectionParam(transaction.context.projectId, transaction.context.connectionId); + const connection = this.connectionInfoResource.get(connectionParam); + const message = typeof info.result === 'string' ? info.result : ''; + + this.notificationService.logInfo({ title: connection?.name ?? info.name ?? '', message }); + } + + private getContextTransaction() { + const context = this.connectionSchemaManagerService.activeExecutionContext; + + if (!context) { + return; + } + + return this.connectionExecutionContextService.get(context.id); + } + + private async disconnectHandler(data: IConnectionExecutorData, contexts: IExecutionContextProvider) { + if (data.state === 'before') { + for (const connectionKey of data.connections) { + const context = this.connectionExecutionContextResource.values.find(connection => isConnectionInfoParamEqual(connection, connectionKey)); + + if (context) { + const transaction = this.connectionExecutionContextService.get(context.id); + + if (transaction?.autoCommit === false) { + const connectionData = this.connectionInfoResource.get(connectionKey); + const state = await this.commonDialogService.open(ConfirmationDialog, { + title: `${this.localizationService.translate('plugin_datasource_transaction_manager_commit')} (${connectionData?.name ?? context.id})`, + message: 'plugin_datasource_transaction_manager_commit_confirmation_message', + confirmActionText: 'plugin_datasource_transaction_manager_commit', + extraStatus: 'no', + }); + + if (state === DialogueStateResult.Resolved) { + await this.commit(transaction, () => ExecutorInterrupter.interrupt(contexts)); + } else if (state === DialogueStateResult.Rejected) { + ExecutorInterrupter.interrupt(contexts); + } + } + } + } + } + } + + private async rollback(transaction: ConnectionExecutionContext) { + try { + const result = await transaction.rollback(); + this.showTransactionResult(transaction, result); + } catch (exception: any) { + this.notificationService.logException(exception, 'plugin_datasource_transaction_manager_rollback_fail'); + } + } + + private async commit(transaction: ConnectionExecutionContext, onError?: (exception: any) => void) { + try { + const result = await transaction.commit(); + this.showTransactionResult(transaction, result); + } catch (exception: any) { + this.notificationService.logException(exception, 'plugin_datasource_transaction_manager_commit_fail'); + onError?.(exception); + } + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerService.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerService.ts new file mode 100644 index 0000000000..1622240cc2 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerService.ts @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { ConnectionExecutionContextService } from '@cloudbeaver/core-connections'; +import { injectable } from '@cloudbeaver/core-di'; +import { ConnectionSchemaManagerService } from '@cloudbeaver/plugin-datasource-context-switch'; + +@injectable(() => [ConnectionSchemaManagerService, ConnectionExecutionContextService]) +export class TransactionManagerService { + constructor( + private readonly connectionSchemaManagerService: ConnectionSchemaManagerService, + private readonly connectionExecutionContextService: ConnectionExecutionContextService, + ) {} + + getActiveContextTransaction() { + const context = this.connectionSchemaManagerService.activeExecutionContext; + + if (!context) { + return; + } + + return this.connectionExecutionContextService.get(context.id); + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerSettingsService.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerSettingsService.ts new file mode 100644 index 0000000000..d6882ac39a --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerSettingsService.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { injectable } from '@cloudbeaver/core-di'; +import { SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; + +const defaultSettings = schema.object({ + 'plugin.datasource-transaction-manager.disabled': schemaExtra.stringedBoolean().default(false), + 'plugin.datasource-transaction-manager.allowCommitModeSwitch': schemaExtra.stringedBoolean().default(true), +}); + +@injectable(() => [SettingsProviderService]) +export class TransactionManagerSettingsService { + get disabled(): boolean { + return this.settings.getValue('plugin.datasource-transaction-manager.disabled'); + } + + get allowCommitModeSwitch(): boolean { + return this.settings.getValue('plugin.datasource-transaction-manager.allowCommitModeSwitch'); + } + + readonly settings: SettingsProvider; + + constructor(private readonly settingsProviderService: SettingsProviderService) { + this.settings = this.settingsProviderService.createSettings(defaultSettings); + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT.ts b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT.ts new file mode 100644 index 0000000000..ed5c81d8d9 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATASOURCE_TRANSACTION_COMMIT = createAction('datasource-transaction-commit', { + label: 'plugin_datasource_transaction_manager_commit', + tooltip: 'plugin_datasource_transaction_manager_commit', + icon: '/icons/commit_m.svg', +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE.ts b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE.ts new file mode 100644 index 0000000000..66f485f534 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE = createAction('datasource-transaction-commit-mode-toggle', { + label: 'plugin_datasource_transaction_manager_commit_mode_switch_to_manual', + tooltip: 'plugin_datasource_transaction_manager_commit_mode_switch_to_manual', + icon: '/icons/commit_mode_auto_m.svg', +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_ROLLBACK.ts b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_ROLLBACK.ts new file mode 100644 index 0000000000..e801026795 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_ROLLBACK.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATASOURCE_TRANSACTION_ROLLBACK = createAction('datasource-transaction-rollback', { + label: 'plugin_datasource_transaction_manager_rollback', + tooltip: 'plugin_datasource_transaction_manager_rollback', + icon: '/icons/rollback_m.svg', +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/index.ts b/webapp/packages/plugin-datasource-transaction-manager/src/index.ts new file mode 100644 index 0000000000..07cd818ca5 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/index.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +export { datasourceTransactionManagerPlugin } from './manifest.js'; + +export { TransactionManagerSettingsService } from './TransactionManagerSettingsService.js'; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/en.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/en.ts new file mode 100644 index 0000000000..7c86fee92f --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/en.ts @@ -0,0 +1,21 @@ +export default [ + ['plugin_datasource_transaction_manager_commit', 'Commit'], + ['plugin_datasource_transaction_manager_rollback', 'Rollback'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_auto', 'Switch to auto-commit'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_manual', 'Switch to manual commit'], + ['plugin_datasource_transaction_manager_commit_fail', 'Failed to commit transaction'], + ['plugin_datasource_transaction_manager_rollback_fail', 'Failed to rollback transaction'], + ['plugin_datasource_transaction_manager_commit_mode_fail', 'Failed to change commit mode'], + ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Do you want to commit changes?'], + + ['plugin_datasource_transaction_manager_logs', 'Transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip', 'Open transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip_count', 'Total statements: {arg:count}'], + ['plugin_datasource_transaction_manager_logs_counter_none', 'None'], + ['plugin_datasource_transaction_manager_logs_table_column_time', 'Time'], + ['plugin_datasource_transaction_manager_logs_table_column_type', 'Type'], + ['plugin_datasource_transaction_manager_logs_table_column_text', 'Query'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Duration (ms)'], + ['plugin_datasource_transaction_manager_logs_table_column_rows', 'Rows'], + ['plugin_datasource_transaction_manager_logs_table_column_result', 'Result'], +]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/fr.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/fr.ts new file mode 100644 index 0000000000..84fbd77a46 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/fr.ts @@ -0,0 +1,21 @@ +export default [ + ['plugin_datasource_transaction_manager_commit', 'Commit'], + ['plugin_datasource_transaction_manager_rollback', 'Rollback'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_auto', 'Passer en mode auto-commit'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_manual', 'Passer en mode commit manuel'], + ['plugin_datasource_transaction_manager_commit_fail', 'Échec de la transaction de commit'], + ['plugin_datasource_transaction_manager_rollback_fail', "Échec de l'annulation de la transaction"], + ['plugin_datasource_transaction_manager_commit_mode_fail', 'Échec du changement de mode de commit'], + ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Voulez-vous commiter les modifications ?'], + + ['plugin_datasource_transaction_manager_logs', 'Transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip', 'Open transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip_count', 'Total statements: {arg:count}'], + ['plugin_datasource_transaction_manager_logs_counter_none', 'None'], + ['plugin_datasource_transaction_manager_logs_table_column_time', 'Time'], + ['plugin_datasource_transaction_manager_logs_table_column_type', 'Type'], + ['plugin_datasource_transaction_manager_logs_table_column_text', 'Query'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Duration (ms)'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Rows'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Result'], +]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/it.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/it.ts new file mode 100644 index 0000000000..304e324793 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/it.ts @@ -0,0 +1,21 @@ +export default [ + ['plugin_datasource_transaction_manager_commit', 'Commit'], + ['plugin_datasource_transaction_manager_rollback', 'Rollback'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_auto', 'Switch to auto-commit'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_manual', 'Switch to manual commit'], + ['plugin_datasource_transaction_manager_commit_fail', 'Failed to commit transaction'], + ['plugin_datasource_transaction_manager_rollback_fail', 'Failed to rollback transaction'], + ['plugin_datasource_transaction_manager_commit_mode_fail', 'Failed to change commit mode'], + ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Do you want to commit changes?'], + + ['plugin_datasource_transaction_manager_logs', 'Transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip', 'Open transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip_count', 'Total statements: {arg:count}'], + ['plugin_datasource_transaction_manager_logs_counter_none', 'None'], + ['plugin_datasource_transaction_manager_logs_table_column_time', 'Time'], + ['plugin_datasource_transaction_manager_logs_table_column_type', 'Type'], + ['plugin_datasource_transaction_manager_logs_table_column_text', 'Query'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Duration (ms)'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Rows'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Result'], +]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/ru.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/ru.ts new file mode 100644 index 0000000000..78547b6b8c --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/ru.ts @@ -0,0 +1,21 @@ +export default [ + ['plugin_datasource_transaction_manager_commit', 'Commit'], + ['plugin_datasource_transaction_manager_rollback', 'Rollback'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_auto', 'Переключить в авто-коммит'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_manual', 'Переключить в ручной коммит'], + ['plugin_datasource_transaction_manager_commit_fail', 'Не удалось выполнить коммит'], + ['plugin_datasource_transaction_manager_rollback_fail', 'Не удалось выполнить откат'], + ['plugin_datasource_transaction_manager_commit_mode_fail', 'Не удалось переключить режим коммита'], + ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Вы хотите зафиксировать изменения?'], + + ['plugin_datasource_transaction_manager_logs', 'Журнал транзакции'], + ['plugin_datasource_transaction_manager_logs_tooltip', 'Открыть журнал транзакции'], + ['plugin_datasource_transaction_manager_logs_tooltip_count', 'Всего запросов: {arg:count}'], + ['plugin_datasource_transaction_manager_logs_counter_none', 'Нет'], + ['plugin_datasource_transaction_manager_logs_table_column_time', 'Время'], + ['plugin_datasource_transaction_manager_logs_table_column_type', 'Тип'], + ['plugin_datasource_transaction_manager_logs_table_column_text', 'Запрос'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Продолжительность (мс)'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Строки'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Результат'], +]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/vi.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/vi.ts new file mode 100644 index 0000000000..258557d6f7 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/vi.ts @@ -0,0 +1,21 @@ +export default [ + ['plugin_datasource_transaction_manager_commit', 'Commit'], + ['plugin_datasource_transaction_manager_rollback', 'Hoàn tác'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_auto', 'Chuyển sang commit tự động'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_manual', 'Chuyển sang commit thủ công'], + ['plugin_datasource_transaction_manager_commit_fail', 'Không thể commit giao dịch'], + ['plugin_datasource_transaction_manager_rollback_fail', 'Không thể hoàn tác giao dịch'], + ['plugin_datasource_transaction_manager_commit_mode_fail', 'Không thể thay đổi chế độ commit'], + ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Bạn có muốn commit các thay đổi không?'], + + ['plugin_datasource_transaction_manager_logs', 'Nhật ký giao dịch'], + ['plugin_datasource_transaction_manager_logs_tooltip', 'Mở nhật ký giao dịch'], + ['plugin_datasource_transaction_manager_logs_tooltip_count', 'Tổng số câu lệnh: {arg:count}'], + ['plugin_datasource_transaction_manager_logs_counter_none', 'Không có'], + ['plugin_datasource_transaction_manager_logs_table_column_time', 'Thời gian'], + ['plugin_datasource_transaction_manager_logs_table_column_type', 'Loại'], + ['plugin_datasource_transaction_manager_logs_table_column_text', 'Truy vấn'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Thời gian thực thi (ms)'], + ['plugin_datasource_transaction_manager_logs_table_column_rows', 'Hàng'], + ['plugin_datasource_transaction_manager_logs_table_column_result', 'Kết quả'], +]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/zh.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/zh.ts new file mode 100644 index 0000000000..999b40766f --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/zh.ts @@ -0,0 +1,21 @@ +export default [ + ['plugin_datasource_transaction_manager_commit', '提交'], + ['plugin_datasource_transaction_manager_rollback', '回滚'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_auto', '切换至自动提交'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_manual', '切换至手动提交'], + ['plugin_datasource_transaction_manager_commit_fail', '提交事务失败'], + ['plugin_datasource_transaction_manager_rollback_fail', '回滚事务失败'], + ['plugin_datasource_transaction_manager_commit_mode_fail', '切换提交方式失败'], + ['plugin_datasource_transaction_manager_commit_confirmation_message', '是否提交更改?'], + + ['plugin_datasource_transaction_manager_logs', 'Transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip', 'Open transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip_count', 'Total statements: {arg:count}'], + ['plugin_datasource_transaction_manager_logs_counter_none', 'None'], + ['plugin_datasource_transaction_manager_logs_table_column_time', 'Time'], + ['plugin_datasource_transaction_manager_logs_table_column_type', 'Type'], + ['plugin_datasource_transaction_manager_logs_table_column_text', 'Query'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Duration (ms)'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Rows'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Result'], +]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/manifest.ts b/webapp/packages/plugin-datasource-transaction-manager/src/manifest.ts new file mode 100644 index 0000000000..08455bba08 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/manifest.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; + +export const datasourceTransactionManagerPlugin: PluginManifest = { + info: { + name: 'Datasource transaction manager plugin', + }, +}; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/module.ts b/webapp/packages/plugin-datasource-transaction-manager/src/module.ts new file mode 100644 index 0000000000..6628578ca6 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/module.ts @@ -0,0 +1,31 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { TransactionManagerBootstrap } from './TransactionManagerBootstrap.js'; +import { TransactionManagerSettingsService } from './TransactionManagerSettingsService.js'; +import { TransactionManagerService } from './TransactionManagerService.js'; +import { TransactionLogCountResource } from './TransactionLog/TransactionLogCountResource.js'; +import { LocaleService } from './LocaleService.js'; +import { TransactionLogCountEventHandler } from './TransactionLog/TransactionLogCountEventHandler.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-datasource-transaction-manager', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Dependency, proxy(TransactionManagerSettingsService)) + .addSingleton(Dependency, proxy(TransactionLogCountResource)) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(Bootstrap, TransactionManagerBootstrap) + .addSingleton(TransactionLogCountEventHandler) + .addSingleton(TransactionManagerSettingsService) + .addSingleton(TransactionManagerService) + .addSingleton(TransactionLogCountResource); + }, +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/tsconfig.json b/webapp/packages/plugin-datasource-transaction-manager/tsconfig.json new file mode 100644 index 0000000000..b2c2bd1df7 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/tsconfig.json @@ -0,0 +1,88 @@ +{ + "extends": "@cloudbeaver/tsconfig/tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true + }, + "references": [ + { + "path": "../../common-typescript/@dbeaver/js-helpers" + }, + { + "path": "../core-blocks" + }, + { + "path": "../core-cli" + }, + { + "path": "../core-connections" + }, + { + "path": "../core-di" + }, + { + "path": "../core-dialogs" + }, + { + "path": "../core-events" + }, + { + "path": "../core-executor" + }, + { + "path": "../core-localization" + }, + { + "path": "../core-resource" + }, + { + "path": "../core-root" + }, + { + "path": "../core-sdk" + }, + { + "path": "../core-settings" + }, + { + "path": "../core-ui" + }, + { + "path": "../core-utils" + }, + { + "path": "../core-view" + }, + { + "path": "../plugin-codemirror6" + }, + { + "path": "../plugin-data-grid" + }, + { + "path": "../plugin-datasource-context-switch" + }, + { + "path": "../plugin-sql-editor-new" + }, + { + "path": "../plugin-tools-panel" + }, + { + "path": "../plugin-top-app-bar" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*" + ] +} diff --git a/webapp/packages/plugin-ddl-viewer/package.json b/webapp/packages/plugin-ddl-viewer/package.json index 90755f9216..88c85bac34 100644 --- a/webapp/packages/plugin-ddl-viewer/package.json +++ b/webapp/packages/plugin-ddl-viewer/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-ddl-viewer", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,36 +11,42 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-codemirror6": "~0.1.0", - "@cloudbeaver/plugin-navigation-tree": "~0.1.0", - "@cloudbeaver/plugin-sql-editor": "~0.1.0", - "@cloudbeaver/plugin-sql-editor-navigation-tab": "~0.1.0", - "@cloudbeaver/plugin-sql-editor-new": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-data-context": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-navigation-tree": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-codemirror6": "workspace:*", + "@cloudbeaver/plugin-navigation-tree": "workspace:*", + "@cloudbeaver/plugin-sql-editor": "workspace:*", + "@cloudbeaver/plugin-sql-editor-navigation-tab": "workspace:*", + "@cloudbeaver/plugin-sql-editor-new": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_NODE.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_NODE.ts index 0e6a73d4aa..b8b4641bcb 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_NODE.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_NODE.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_VALUE.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_VALUE.ts index fc28ba4daa..ac2e51d766 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_VALUE.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DATA_CONTEXT_DDL_VIEWER_VALUE.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts index ced80f0a16..8e4ead0170 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerFooterService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,16 +8,16 @@ import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; -import { download, generateFileName } from '@cloudbeaver/core-utils'; -import { ACTION_SAVE, ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view'; +import { download, withTimestamp } from '@cloudbeaver/core-utils'; +import { ACTION_SAVE, ActionService, MenuService } from '@cloudbeaver/core-view'; import { LocalStorageSqlDataSource } from '@cloudbeaver/plugin-sql-editor'; import { ACTION_SQL_EDITOR_OPEN, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab'; -import { DATA_CONTEXT_DDL_VIEWER_NODE } from './DATA_CONTEXT_DDL_VIEWER_NODE'; -import { DATA_CONTEXT_DDL_VIEWER_VALUE } from './DATA_CONTEXT_DDL_VIEWER_VALUE'; -import { MENU_DDL_VIEWER_FOOTER } from './MENU_DDL_VIEWER_FOOTER'; +import { DATA_CONTEXT_DDL_VIEWER_NODE } from './DATA_CONTEXT_DDL_VIEWER_NODE.js'; +import { DATA_CONTEXT_DDL_VIEWER_VALUE } from './DATA_CONTEXT_DDL_VIEWER_VALUE.js'; +import { MENU_DDL_VIEWER_FOOTER } from './MENU_DDL_VIEWER_FOOTER.js'; -@injectable() +@injectable(() => [NavNodeManagerService, ActionService, MenuService, SqlEditorNavigatorService, ConnectionInfoResource]) export class DDLViewerFooterService { constructor( private readonly navNodeManagerService: NavNodeManagerService, @@ -30,25 +30,13 @@ export class DDLViewerFooterService { register(): void { this.actionsService.addHandler({ id: 'ddl-viewer-footer-base-handler', - isActionApplicable(context, action) { - const menu = context.hasValue(DATA_CONTEXT_MENU, MENU_DDL_VIEWER_FOOTER); - const node = context.tryGet(DATA_CONTEXT_DDL_VIEWER_NODE); - - if (!menu || !node) { - return false; - } - - if (action === ACTION_SAVE || action === ACTION_SQL_EDITOR_OPEN) { - const ddl = context.tryGet(DATA_CONTEXT_DDL_VIEWER_VALUE); - return !!ddl; - } - - return false; - }, + menus: [MENU_DDL_VIEWER_FOOTER], + contexts: [DATA_CONTEXT_DDL_VIEWER_NODE, DATA_CONTEXT_DDL_VIEWER_VALUE], + actions: [ACTION_SAVE, ACTION_SQL_EDITOR_OPEN], handler: async (context, action) => { switch (action) { case ACTION_SAVE: { - const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_VALUE); + const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_VALUE)!; const nodeId = context.get(DATA_CONTEXT_DDL_VIEWER_NODE); const blob = new Blob([ddl], { @@ -58,12 +46,12 @@ export class DDLViewerFooterService { const node = nodeId ? this.navNodeManagerService.getNode(nodeId) : undefined; const name = node?.name ? `DDL_${node.nodeType ? node.nodeType + '_' : ''}${node.name}` : 'DDL'; - download(blob, generateFileName(name, '.sql')); + download(blob, `${withTimestamp(name)}.sql`); break; } case ACTION_SQL_EDITOR_OPEN: { - const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_VALUE); - const nodeId = context.get(DATA_CONTEXT_DDL_VIEWER_NODE); + const ddl = context.get(DATA_CONTEXT_DDL_VIEWER_VALUE)!; + const nodeId = context.get(DATA_CONTEXT_DDL_VIEWER_NODE)!; const connection = this.connectionInfoResource.getConnectionForNode(nodeId); const container = this.navNodeManagerService.getNodeContainerInfo(nodeId); @@ -101,7 +89,7 @@ export class DDLViewerFooterService { }); this.menuService.addCreator({ - isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_DDL_VIEWER_FOOTER, + menus: [MENU_DDL_VIEWER_FOOTER], getItems: (context, items) => [...items, ACTION_SAVE, ACTION_SQL_EDITOR_OPEN], }); } diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTab.tsx b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTab.tsx index 3a623fcc08..a009271679 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTab.tsx +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTab.tsx @@ -1,26 +1,24 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled from 'reshadow'; -import { useStyles } from '@cloudbeaver/core-blocks'; -import { Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; +import { TabIcon, Tab, TabTitle } from '@cloudbeaver/core-ui'; import type { NavNodeTransformViewComponent } from '@cloudbeaver/plugin-navigation-tree'; -import { NAV_NODE_DDL_ID } from '../NAV_NODE_DDL_ID'; +import { NAV_NODE_DDL_ID } from '../NAV_NODE_DDL_ID.js'; -export const DDLViewerTab: NavNodeTransformViewComponent = observer(function DDLViewerTab({ folderId, style }) { +export const DDLViewerTab: NavNodeTransformViewComponent = observer(function DDLViewerTab({ folderId }) { const title = folderId.startsWith(NAV_NODE_DDL_ID) ? 'DDL' : 'Body'; - return styled(useStyles(style))( + return ( {title} - , + ); }); diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.m.css b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.m.css deleted file mode 100644 index 5e763e22d4..0000000000 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.m.css +++ /dev/null @@ -1,17 +0,0 @@ -.wrapper { - composes: theme-typography--body1 from global; - flex: 1; - display: flex; - flex-direction: column; - overflow: auto; -} - -.sqlCodeEditorLoader { - height: 100%; - flex: 1; - overflow: auto; -} - -.menuBar { - border-top: 1px solid var(--theme-background); -} diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.module.css b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.module.css new file mode 100644 index 0000000000..6e49ce4fda --- /dev/null +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.module.css @@ -0,0 +1,24 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.wrapper { + composes: theme-typography--body1 from global; + flex: 1; + display: flex; + flex-direction: column; + overflow: auto; +} + +.sqlCodeEditorLoader { + height: 100%; + flex: 1; + overflow: auto; +} + +.menuBar { + border-top: 1px solid var(--theme-background); +} diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx index f80fccc49c..28a4dc7c28 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -14,20 +14,21 @@ import { ConnectionInfoResource, createConnectionParam, } from '@cloudbeaver/core-connections'; -import { MenuBar } from '@cloudbeaver/core-ui'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; +import { MenuBar, MenuBarItemStyles, MenuBarStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; import type { NavNodeTransformViewComponent } from '@cloudbeaver/plugin-navigation-tree'; import { SQLCodeEditorLoader, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-new'; -import { DATA_CONTEXT_DDL_VIEWER_NODE } from './DATA_CONTEXT_DDL_VIEWER_NODE'; -import { DATA_CONTEXT_DDL_VIEWER_VALUE } from './DATA_CONTEXT_DDL_VIEWER_VALUE'; -import { DdlResource } from './DdlResource'; -import style from './DDLViewerTabPanel.m.css'; -import { MENU_DDL_VIEWER_FOOTER } from './MENU_DDL_VIEWER_FOOTER'; +import { DATA_CONTEXT_DDL_VIEWER_NODE } from './DATA_CONTEXT_DDL_VIEWER_NODE.js'; +import { DATA_CONTEXT_DDL_VIEWER_VALUE } from './DATA_CONTEXT_DDL_VIEWER_VALUE.js'; +import { DdlResource } from './DdlResource.js'; +import style from './DDLViewerTabPanel.module.css'; +import { MENU_DDL_VIEWER_FOOTER } from './MENU_DDL_VIEWER_FOOTER.js'; export const DDLViewerTabPanel: NavNodeTransformViewComponent = observer(function DDLViewerTabPanel({ nodeId, folderId }) { - const styles = useS(style); + const styles = useS(style, MenuBarStyles, MenuBarItemStyles); const menu = useMenu({ menu: MENU_DDL_VIEWER_FOOTER }); const ddlResource = useResource(DDLViewerTabPanel, DdlResource, nodeId); @@ -39,14 +40,17 @@ export const DDLViewerTabPanel: NavNodeTransformViewComponent = observer(functio const sqlDialect = useSqlDialectExtension(connectionDialectResource.data); const extensions = useCodemirrorExtensions(); extensions.set(...sqlDialect); + const ddlData = ddlResource.data; - menu.context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId); - menu.context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, ddlResource.data); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId, id); + context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, ddlData, id); + }); return (
- +
); }); diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DdlResource.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DdlResource.ts index a2a9c351b7..c6a8ecb43a 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DdlResource.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DdlResource.ts @@ -1,18 +1,21 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; -import { CachedMapResource, isResourceAlias, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { CachedMapResource, isResourceAlias, type ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import { GraphQLService } from '@cloudbeaver/core-sdk'; -@injectable() +@injectable(() => [GraphQLService, NavNodeInfoResource]) export class DdlResource extends CachedMapResource { - constructor(private readonly graphQLService: GraphQLService, private readonly navNodeInfoResource: NavNodeInfoResource) { + constructor( + private readonly graphQLService: GraphQLService, + private readonly navNodeInfoResource: NavNodeInfoResource, + ) { super(); this.navNodeInfoResource.outdateResource(this); diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/MENU_DDL_VIEWER_FOOTER.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/MENU_DDL_VIEWER_FOOTER.ts index 4a3100bd3b..35b0e54543 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/MENU_DDL_VIEWER_FOOTER.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/MENU_DDL_VIEWER_FOOTER.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const MENU_DDL_VIEWER_FOOTER = createMenu('ddl-viewer-footer', 'DDL viewer footer menu'); +export const MENU_DDL_VIEWER_FOOTER = createMenu('ddl-viewer-footer', { label: 'DDL viewer footer menu' }); diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts index 3308dd09e3..c8d3a2a416 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts @@ -1,22 +1,26 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { EObjectFeature, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; import { NavNodeViewService } from '@cloudbeaver/plugin-navigation-tree'; -import { DDLViewerFooterService } from './DdlViewer/DDLViewerFooterService'; -import { DDLViewerTab } from './DdlViewer/DDLViewerTab'; -import { DDLViewerTabPanel } from './DdlViewer/DDLViewerTabPanel'; -import { ExtendedDDLViewerTabPanel } from './ExtendedDDLViewer/ExtendedDDLViewerTabPanel'; -import { NAV_NODE_DDL_ID } from './NAV_NODE_DDL_ID'; -import { NAV_NODE_EXTENDED_DDL_ID } from './NAV_NODE_EXTENDED_DDL_ID'; +import { DDLViewerFooterService } from './DdlViewer/DDLViewerFooterService.js'; +import { NAV_NODE_DDL_ID } from './NAV_NODE_DDL_ID.js'; +import { NAV_NODE_EXTENDED_DDL_ID } from './NAV_NODE_EXTENDED_DDL_ID.js'; -@injectable() +const DDLViewerTab = importLazyComponent(() => import('./DdlViewer/DDLViewerTab.js').then(m => m.DDLViewerTab)); +const DDLViewerTabPanel = importLazyComponent(() => import('./DdlViewer/DDLViewerTabPanel.js').then(m => m.DDLViewerTabPanel)); +const ExtendedDDLViewerTabPanel = importLazyComponent(() => + import('./ExtendedDDLViewer/ExtendedDDLViewerTabPanel.js').then(m => m.ExtendedDDLViewerTabPanel), +); + +@injectable(() => [NavNodeViewService, NavNodeInfoResource, DDLViewerFooterService]) export class DdlViewerBootstrap extends Bootstrap { constructor( private readonly navNodeViewService: NavNodeViewService, @@ -26,7 +30,7 @@ export class DdlViewerBootstrap extends Bootstrap { super(); } - register(): void { + override register(): void { this.navNodeViewService.addTransform({ tab: (nodeId, folderId) => { if (folderId.startsWith(NAV_NODE_DDL_ID) || folderId.startsWith(NAV_NODE_EXTENDED_DDL_ID)) { @@ -64,6 +68,4 @@ export class DdlViewerBootstrap extends Bootstrap { this.ddlViewerFooterService.register(); } - - load(): void {} } diff --git a/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLResource.ts b/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLResource.ts index f419ddd819..2ac777676e 100644 --- a/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLResource.ts +++ b/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLResource.ts @@ -1,18 +1,21 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; -import { CachedMapResource, isResourceAlias, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { CachedMapResource, isResourceAlias, type ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import { GraphQLService } from '@cloudbeaver/core-sdk'; -@injectable() +@injectable(() => [GraphQLService, NavNodeInfoResource]) export class ExtendedDDLResource extends CachedMapResource { - constructor(private readonly graphQLService: GraphQLService, private readonly navNodeInfoResource: NavNodeInfoResource) { + constructor( + private readonly graphQLService: GraphQLService, + private readonly navNodeInfoResource: NavNodeInfoResource, + ) { super(); this.navNodeInfoResource.outdateResource(this); diff --git a/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx b/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx index 3c192d3f71..bd9ff42aa4 100644 --- a/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx +++ b/webapp/packages/plugin-ddl-viewer/src/ExtendedDDLViewer/ExtendedDDLViewerTabPanel.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -14,17 +14,18 @@ import { ConnectionInfoResource, createConnectionParam, } from '@cloudbeaver/core-connections'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import { MenuBar } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; import type { NavNodeTransformViewComponent } from '@cloudbeaver/plugin-navigation-tree'; import { SQLCodeEditorLoader, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-new'; -import { DATA_CONTEXT_DDL_VIEWER_NODE } from '../DdlViewer/DATA_CONTEXT_DDL_VIEWER_NODE'; -import { DATA_CONTEXT_DDL_VIEWER_VALUE } from '../DdlViewer/DATA_CONTEXT_DDL_VIEWER_VALUE'; -import style from '../DdlViewer/DDLViewerTabPanel.m.css'; -import { MENU_DDL_VIEWER_FOOTER } from '../DdlViewer/MENU_DDL_VIEWER_FOOTER'; -import { ExtendedDDLResource } from './ExtendedDDLResource'; +import { DATA_CONTEXT_DDL_VIEWER_NODE } from '../DdlViewer/DATA_CONTEXT_DDL_VIEWER_NODE.js'; +import { DATA_CONTEXT_DDL_VIEWER_VALUE } from '../DdlViewer/DATA_CONTEXT_DDL_VIEWER_VALUE.js'; +import style from '../DdlViewer/DDLViewerTabPanel.module.css'; +import { MENU_DDL_VIEWER_FOOTER } from '../DdlViewer/MENU_DDL_VIEWER_FOOTER.js'; +import { ExtendedDDLResource } from './ExtendedDDLResource.js'; export const ExtendedDDLViewerTabPanel: NavNodeTransformViewComponent = observer(function ExtendedDDLViewerTabPanel({ nodeId, folderId }) { const styles = useS(style); @@ -39,18 +40,16 @@ export const ExtendedDDLViewerTabPanel: NavNodeTransformViewComponent = observer const sqlDialect = useSqlDialectExtension(connectionDialectResource.data); const extensions = useCodemirrorExtensions(); extensions.set(...sqlDialect); + const extendedDDlData = extendedDDLResource.data; - menu.context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId); - menu.context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, extendedDDLResource.data); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId, id); + context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, extendedDDlData, id); + }); return (
- +
); diff --git a/webapp/packages/plugin-ddl-viewer/src/NAV_NODE_DDL_ID.ts b/webapp/packages/plugin-ddl-viewer/src/NAV_NODE_DDL_ID.ts index 9aa910ef9c..bc3dfa331d 100644 --- a/webapp/packages/plugin-ddl-viewer/src/NAV_NODE_DDL_ID.ts +++ b/webapp/packages/plugin-ddl-viewer/src/NAV_NODE_DDL_ID.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-ddl-viewer/src/NAV_NODE_EXTENDED_DDL_ID.ts b/webapp/packages/plugin-ddl-viewer/src/NAV_NODE_EXTENDED_DDL_ID.ts index 911f01d731..7332e1c76e 100644 --- a/webapp/packages/plugin-ddl-viewer/src/NAV_NODE_EXTENDED_DDL_ID.ts +++ b/webapp/packages/plugin-ddl-viewer/src/NAV_NODE_EXTENDED_DDL_ID.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-ddl-viewer/src/TAB_PANEL_STYLES.ts b/webapp/packages/plugin-ddl-viewer/src/TAB_PANEL_STYLES.ts deleted file mode 100644 index cc939e9874..0000000000 --- a/webapp/packages/plugin-ddl-viewer/src/TAB_PANEL_STYLES.ts +++ /dev/null @@ -1,28 +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 TAB_PANEL_STYLES = css` - wrapper { - flex: 1; - display: flex; - flex-direction: column; - overflow: auto; - composes: theme-typography--body1 from global; - } - - SQLCodeEditorLoader { - height: 100%; - flex: 1; - overflow: auto; - } - - MenuBar { - border-top: 1px solid var(--theme-background); - } -`; diff --git a/webapp/packages/plugin-ddl-viewer/src/index.ts b/webapp/packages/plugin-ddl-viewer/src/index.ts index 9e5e6ba322..0253432608 100644 --- a/webapp/packages/plugin-ddl-viewer/src/index.ts +++ b/webapp/packages/plugin-ddl-viewer/src/index.ts @@ -1,4 +1,13 @@ -import { manifest } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { manifest } from './manifest.js'; // All Services and Components that is provided by this plugin should be exported here diff --git a/webapp/packages/plugin-ddl-viewer/src/manifest.ts b/webapp/packages/plugin-ddl-viewer/src/manifest.ts index 9ed9f2be61..0902701fee 100644 --- a/webapp/packages/plugin-ddl-viewer/src/manifest.ts +++ b/webapp/packages/plugin-ddl-viewer/src/manifest.ts @@ -1,21 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { DdlResource } from './DdlViewer/DdlResource'; -import { DDLViewerFooterService } from './DdlViewer/DDLViewerFooterService'; -import { DdlViewerBootstrap } from './DdlViewerBootstrap'; -import { ExtendedDDLResource } from './ExtendedDDLViewer/ExtendedDDLResource'; - export const manifest: PluginManifest = { info: { name: 'DDL Viewer Plugin', }, - - providers: [DdlViewerBootstrap, DDLViewerFooterService, ExtendedDDLResource, DdlResource], }; diff --git a/webapp/packages/plugin-ddl-viewer/src/module.ts b/webapp/packages/plugin-ddl-viewer/src/module.ts new file mode 100644 index 0000000000..7fd86bd7e3 --- /dev/null +++ b/webapp/packages/plugin-ddl-viewer/src/module.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { ExtendedDDLResource } from './ExtendedDDLViewer/ExtendedDDLResource.js'; +import { DdlViewerBootstrap } from './DdlViewerBootstrap.js'; +import { DdlResource } from './DdlViewer/DdlResource.js'; +import { DDLViewerFooterService } from './DdlViewer/DDLViewerFooterService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-ddl-viewer', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, DdlViewerBootstrap) + .addSingleton(Dependency, proxy(ExtendedDDLResource)) + .addSingleton(Dependency, proxy(DdlResource)) + .addSingleton(DDLViewerFooterService) + .addSingleton(ExtendedDDLResource) + .addSingleton(DdlResource); + }, +}); diff --git a/webapp/packages/plugin-ddl-viewer/tsconfig.json b/webapp/packages/plugin-ddl-viewer/tsconfig.json index 6cc167fbde..5b47f5a1b9 100644 --- a/webapp/packages/plugin-ddl-viewer/tsconfig.json +++ b/webapp/packages/plugin-ddl-viewer/tsconfig.json @@ -1,55 +1,59 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-codemirror6/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../plugin-navigation-tree/tsconfig.json" + "path": "../core-cli" }, { - "path": "../plugin-sql-editor/tsconfig.json" + "path": "../core-connections" }, { - "path": "../plugin-sql-editor-navigation-tab/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../plugin-sql-editor-new/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-navigation-tree" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-data-context/tsconfig.json" + "path": "../core-sdk" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-navigation-tree/tsconfig.json" + "path": "../core-utils" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-view" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../plugin-codemirror6" }, { - "path": "../core-ui/tsconfig.json" + "path": "../plugin-navigation-tree" }, { - "path": "../core-utils/tsconfig.json" + "path": "../plugin-sql-editor" }, { - "path": "../core-view/tsconfig.json" + "path": "../plugin-sql-editor-navigation-tab" + }, + { + "path": "../plugin-sql-editor-new" } ], "include": [ @@ -61,7 +65,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-devtools/package.json b/webapp/packages/plugin-devtools/package.json index bd2eecaf76..ab8eb42b99 100644 --- a/webapp/packages/plugin-devtools/package.json +++ b/webapp/packages/plugin-devtools/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-devtools", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,32 +11,39 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-settings-menu": "~0.1.0", - "@cloudbeaver/plugin-user-profile": "~0.1.0", - "@cloudbeaver/core-authentication": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-data-context": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "react": "~18.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-storage": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-settings-menu": "workspace:*", + "@cloudbeaver/plugin-user-profile": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "tslib": "^2", + "typescript": "^5", + "typescript-plugin-css-modules": "^5", + "vite": "^7" + } } diff --git a/webapp/packages/plugin-devtools/src/ContextMenu/DATA_CONTEXT_MENU_SEARCH.ts b/webapp/packages/plugin-devtools/src/ContextMenu/DATA_CONTEXT_MENU_SEARCH.ts index 23b9729de9..358da668fa 100644 --- a/webapp/packages/plugin-devtools/src/ContextMenu/DATA_CONTEXT_MENU_SEARCH.ts +++ b/webapp/packages/plugin-devtools/src/ContextMenu/DATA_CONTEXT_MENU_SEARCH.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { createDataContext } from '@cloudbeaver/core-data-context'; -export const DATA_CONTEXT_MENU_SEARCH = createDataContext('menu-local', () => ''); +export const DATA_CONTEXT_MENU_SEARCH = createDataContext('menu-search'); diff --git a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItem.ts b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItem.ts index c16c751b71..4d40c111bb 100644 --- a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItem.ts +++ b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItem.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,7 +8,7 @@ import type { IContextMenuItemProps } from '@cloudbeaver/core-ui'; import { MenuCustomItem } from '@cloudbeaver/core-view'; -import { SearchResourceMenuItemComponent } from './SearchResourceMenuItemComponent'; +import { SearchResourceMenuItemComponent } from './SearchResourceMenuItemComponent.js'; export class SearchResourceMenuItem extends MenuCustomItem { constructor() { diff --git a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.module.css b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.module.css new file mode 100644 index 0000000000..d8ef75b08d --- /dev/null +++ b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.module.css @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.searchBox { + padding: 8px 12px; +} diff --git a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx index 9830738a76..6d67352f3c 100644 --- a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx +++ b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx @@ -1,46 +1,49 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; +import { useRef } from 'react'; -import { useStyles } from '@cloudbeaver/core-blocks'; +import { s, useS } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; import type { IContextMenuItemProps } from '@cloudbeaver/core-ui'; import type { ICustomMenuItemComponent } from '@cloudbeaver/core-view'; -import { DATA_CONTEXT_MENU_SEARCH } from './DATA_CONTEXT_MENU_SEARCH'; - -const styles = css` - search-box { - padding: 8px 12px; - } -`; +import { DATA_CONTEXT_MENU_SEARCH } from './DATA_CONTEXT_MENU_SEARCH.js'; +import styles from './SearchResourceMenuItemComponent.module.css'; export const SearchResourceMenuItemComponent: ICustomMenuItemComponent = observer(function SearchResourceMenuItemComponent({ - item, - onClick, - menuData, + context, className, - style, }) { - const value = menuData.context.tryGet(DATA_CONTEXT_MENU_SEARCH) ?? ''; + const style = useS(styles); + const value = context.get(DATA_CONTEXT_MENU_SEARCH) ?? ''; + const contextRefId = useRef(null); + + useDataContextLink(context, (context, id) => { + contextRefId.current = id; + }); + function handleChange(value: string) { - menuData.context.set(DATA_CONTEXT_MENU_SEARCH, value); + if (contextRefId.current) { + context.set(DATA_CONTEXT_MENU_SEARCH, value, contextRefId.current); + } } - return styled(useStyles(style, styles))( - + return ( +
handleChange(event.target.value)} /> - , +
); }); diff --git a/webapp/packages/plugin-devtools/src/DevToolsService.ts b/webapp/packages/plugin-devtools/src/DevToolsService.ts index 9d0f33e5cf..758fad81bc 100644 --- a/webapp/packages/plugin-devtools/src/DevToolsService.ts +++ b/webapp/packages/plugin-devtools/src/DevToolsService.ts @@ -1,59 +1,95 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +/// import { makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; +import { StorageService } from '@cloudbeaver/core-storage'; interface IDevToolsSettings { enabled: boolean; - distributed: boolean; + override: boolean; + distributed: boolean | null; + configuration: boolean | null; } const DEVTOOLS = 'devtools'; -@injectable() +@injectable(() => [ServerConfigResource, StorageService]) export class DevToolsService { get isEnabled(): boolean { return this.settings.enabled; } + get isOverride(): boolean { + return this.settings.override; + } + get isDistributed(): boolean { - return this.settings.distributed; + if (!this.isOverride) { + return this.serverConfigResource.data?.distributed ?? false; + } + + return this.settings.distributed ?? this.serverConfigResource.data?.distributed ?? false; + } + + get isConfiguration(): boolean { + if (!this.isOverride) { + return this.serverConfigResource.data?.configurationMode ?? false; + } + return this.settings.configuration ?? this.serverConfigResource.data?.configurationMode ?? false; } private readonly settings: IDevToolsSettings; - constructor(private readonly serverConfigResource: ServerConfigResource, private readonly autoSaveService: LocalStorageSaveService) { + constructor( + private readonly serverConfigResource: ServerConfigResource, + private readonly storageService: StorageService, + ) { this.settings = getDefaultDevToolsSettings(); makeObservable(this, { settings: observable, }); - this.autoSaveService.withAutoSave(DEVTOOLS, this.settings, getDefaultDevToolsSettings); - this.serverConfigResource.onDataUpdate.addHandler(this.syncDistributedMode.bind(this)); + this.storageService.registerSettings(DEVTOOLS, this.settings, getDefaultDevToolsSettings); + this.serverConfigResource.onDataUpdate.addHandler(this.syncSettingsOverride.bind(this)); } switch() { this.settings.enabled = !this.settings.enabled; - this.syncDistributedMode(); + this.syncSettingsOverride(); + } + + setOverride(override: boolean) { + this.settings.override = override; + this.syncSettingsOverride(); } setDistributedMode(distributed: boolean) { this.settings.distributed = distributed; - this.syncDistributedMode(); + this.syncSettingsOverride(); + } + + setConfigurationMode(configuration: boolean) { + this.settings.configuration = configuration; + this.syncSettingsOverride(); } - private syncDistributedMode() { - if (this.isEnabled) { + private syncSettingsOverride() { + if (this.isOverride && this.isEnabled) { if (this.serverConfigResource.data) { - this.serverConfigResource.data.distributed = this.isDistributed; + if (this.settings.distributed !== null) { + this.serverConfigResource.data.distributed = this.isDistributed; + } + if (this.settings.configuration !== null) { + this.serverConfigResource.data.configurationMode = this.isConfiguration; + } } } } @@ -61,7 +97,9 @@ export class DevToolsService { function getDefaultDevToolsSettings(): IDevToolsSettings { return { - enabled: false, - distributed: false, + enabled: import.meta.env.DEV, + override: false, + distributed: null, + configuration: null, }; } diff --git a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts index ac43aac65e..a1b750ef3a 100644 --- a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts @@ -1,36 +1,31 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { EAdminPermission } from '@cloudbeaver/core-authentication'; -import { App, Bootstrap, DIService, injectable, IServiceConstructor } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable, IServiceProvider } from '@cloudbeaver/core-di'; import { CachedResource } from '@cloudbeaver/core-resource'; -import { PermissionsService } from '@cloudbeaver/core-root'; -import { ActionService, DATA_CONTEXT_MENU, DATA_CONTEXT_SUBMENU_ITEM, MenuBaseItem, MenuService } from '@cloudbeaver/core-view'; +import { EAdminPermission, PermissionsService } from '@cloudbeaver/core-root'; +import { ActionService, DATA_CONTEXT_SUBMENU_ITEM, MenuBaseItem, MenuService } from '@cloudbeaver/core-view'; 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_DISTRIBUTED } from './actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED'; -import { DATA_CONTEXT_MENU_SEARCH } from './ContextMenu/DATA_CONTEXT_MENU_SEARCH'; -import { SearchResourceMenuItem } from './ContextMenu/SearchResourceMenuItem'; -import { DevToolsService } from './DevToolsService'; -import { MENU_DEVTOOLS } from './menu/MENU_DEVTOOLS'; -import { MENU_PLUGIN } from './menu/MENU_PLUGIN'; -import { MENU_PLUGINS } from './menu/MENU_PLUGINS'; -import { MENU_RESOURCE } from './menu/MENU_RESOURCE'; -import { MENU_RESOURCES } from './menu/MENU_RESOURCES'; -import { PluginSubMenuItem } from './menu/PluginSubMenuItem'; -import { ResourceSubMenuItem } from './menu/ResourceSubMenuItem'; - -@injectable() +import { ACTION_DEVTOOLS } from './actions/ACTION_DEVTOOLS.js'; +import { ACTION_DEVTOOLS_MODE_CONFIGURATION } from './actions/ACTION_DEVTOOLS_MODE_CONFIGURATION.js'; +import { ACTION_DEVTOOLS_MODE_DISTRIBUTED } from './actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED.js'; +import { ACTION_DEVTOOLS_OVERRIDE } from './actions/ACTION_DEVTOOLS_OVERRIDE.js'; +import { DevToolsService } from './DevToolsService.js'; +import { MENU_DEVTOOLS } from './menu/MENU_DEVTOOLS.js'; +import { MENU_PLUGINS } from './menu/MENU_PLUGINS.js'; +import { MENU_RESOURCE } from './menu/MENU_RESOURCE.js'; +import { ResourceSubMenuItem } from './menu/ResourceSubMenuItem.js'; + +@injectable(() => [IServiceProvider, MenuService, ActionService, DevToolsService, PermissionsService]) export class PluginBootstrap extends Bootstrap { constructor( - private readonly app: App, - private readonly diService: DIService, + private readonly serviceProvider: IServiceProvider, private readonly menuService: MenuService, private readonly actionService: ActionService, private readonly devToolsService: DevToolsService, @@ -39,14 +34,10 @@ export class PluginBootstrap extends Bootstrap { super(); } - register(): void { + override register(): void { this.menuService.addCreator({ - isApplicable: context => { - if (!this.permissionsService.has(EAdminPermission.admin)) { - return false; - } - return context.get(DATA_CONTEXT_MENU) === TOP_NAV_BAR_SETTINGS_MENU; - }, + menus: [TOP_NAV_BAR_SETTINGS_MENU], + isApplicable: () => this.permissionsService.has(EAdminPermission.admin), getItems: (context, items) => [ACTION_DEVTOOLS, ...items], }); @@ -76,89 +67,111 @@ export class PluginBootstrap extends Bootstrap { // }); this.menuService.addCreator({ - isApplicable: context => { - if (!this.devToolsService.isEnabled) { - return false; - } - return context.get(DATA_CONTEXT_MENU) === MENU_USER_PROFILE; - }, + menus: [MENU_USER_PROFILE], + isApplicable: () => this.devToolsService.isEnabled, getItems: (context, items) => [MENU_DEVTOOLS, ...items], }); this.menuService.addCreator({ - isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_DEVTOOLS, + menus: [MENU_DEVTOOLS], getItems: (context, items) => { - const search = context.tryGet(DATA_CONTEXT_MENU_SEARCH); + // const search = context.get(DATA_CONTEXT_MENU_SEARCH); - if (search) { - return [ - new SearchResourceMenuItem(), - ...this.getResources(this.app.getServices().filter(service => service.name.toLocaleLowerCase().includes(search.toLocaleLowerCase()))), - ]; - } + // if (search) { + // return [ + // new SearchResourceMenuItem(), + // ...this.getResources(this.app.getServices().filter(service => service.name.toLocaleLowerCase().includes(search.toLocaleLowerCase()))), + // ]; + // } - return [new SearchResourceMenuItem(), ACTION_DEVTOOLS_MODE_DISTRIBUTED, MENU_PLUGINS, ...items]; + return [ + // new SearchResourceMenuItem(), + ACTION_DEVTOOLS_OVERRIDE, + ACTION_DEVTOOLS_MODE_DISTRIBUTED, + ACTION_DEVTOOLS_MODE_CONFIGURATION, + MENU_PLUGINS, + ...items, + ]; + }, + }); + + this.actionService.addHandler({ + id: 'devtools-mode-configuration', + actions: [ACTION_DEVTOOLS_MODE_CONFIGURATION], + isChecked: () => this.devToolsService.isConfiguration, + isDisabled: () => !this.devToolsService.isOverride, + handler: () => { + this.devToolsService.setConfigurationMode(!this.devToolsService.isConfiguration); }, }); this.actionService.addHandler({ id: 'devtools-mode-distributed', - isActionApplicable: (context, action) => action === ACTION_DEVTOOLS_MODE_DISTRIBUTED, + actions: [ACTION_DEVTOOLS_MODE_DISTRIBUTED], isChecked: () => this.devToolsService.isDistributed, + isDisabled: () => !this.devToolsService.isOverride, handler: () => { this.devToolsService.setDistributedMode(!this.devToolsService.isDistributed); }, }); - this.menuService.addCreator({ - isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_PLUGINS, - getItems: (context, items) => [ - ...this.app - .getPlugins() - .sort((a, b) => a.info.name.localeCompare(b.info.name)) - .map(plugin => new PluginSubMenuItem(plugin)), - ...items, - ], + this.actionService.addHandler({ + id: 'devtools-override', + actions: [ACTION_DEVTOOLS_OVERRIDE], + isChecked: () => this.devToolsService.isOverride, + handler: () => { + this.devToolsService.setOverride(!this.devToolsService.isOverride); + }, }); - this.menuService.addCreator({ - isApplicable: context => { - if (context.get(DATA_CONTEXT_MENU) !== MENU_PLUGIN) { - return false; - } - const item = context.tryGet(DATA_CONTEXT_SUBMENU_ITEM); + // this.menuService.addCreator({ + // menus: [MENU_PLUGINS], + // getItems: (context, items) => [ + // ...this.app + // .getPlugins() + // .sort((a, b) => a.info.name.localeCompare(b.info.name)) + // .map(plugin => new PluginSubMenuItem(plugin)), + // ...items, + // ], + // }); - if (item instanceof PluginSubMenuItem) { - return item.plugin.providers.some(provider => provider.prototype instanceof CachedResource); - } + // this.menuService.addCreator({ + // menus: [MENU_PLUGIN], + // isApplicable: context => { + // const item = context.get(DATA_CONTEXT_SUBMENU_ITEM); - return false; - }, - getItems: (_, items) => [MENU_RESOURCES, ...items], - }); + // if (item instanceof PluginSubMenuItem) { + // return this.app.getServices(item.plugin).some(service => service.prototype instanceof CachedResource); + // } - this.menuService.addCreator({ - isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_RESOURCES && context.has(DATA_CONTEXT_SUBMENU_ITEM), - getItems: (context, items) => { - const item = context.find(DATA_CONTEXT_SUBMENU_ITEM, item => item instanceof PluginSubMenuItem); + // return false; + // }, + // getItems: (_, items) => [MENU_RESOURCES, ...items], + // }); + + // this.menuService.addCreator({ + // menus: [MENU_RESOURCES], + // contexts: [DATA_CONTEXT_SUBMENU_ITEM], + // getItems: (context, items) => { + // const item = context.find(DATA_CONTEXT_SUBMENU_ITEM, item => item instanceof PluginSubMenuItem); - if (!item) { - return items; - } + // if (!item) { + // return items; + // } - const plugin = this.app.getPlugins().find(plugin => plugin.info.name === item.id); + // const plugin = this.app.getPlugins().find(plugin => plugin.info.name === item.id); - if (!plugin) { - return items; - } + // if (!plugin) { + // return items; + // } - return [...this.getResources(plugin.providers), ...items]; - }, - }); + // return [...this.getResources(this.app.getServices(plugin)), ...items]; + // }, + // }); this.menuService.addCreator({ - isApplicable: context => - context.get(DATA_CONTEXT_MENU) === MENU_RESOURCE && context.get(DATA_CONTEXT_SUBMENU_ITEM) instanceof ResourceSubMenuItem, + menus: [MENU_RESOURCE], + isApplicable: context => context.get(DATA_CONTEXT_SUBMENU_ITEM) instanceof ResourceSubMenuItem, getItems: (context, items) => { const item = context.get(DATA_CONTEXT_SUBMENU_ITEM) as ResourceSubMenuItem; @@ -171,7 +184,7 @@ export class PluginBootstrap extends Bootstrap { }, { onSelect: () => { - const instance = this.diService.serviceInjector.getServiceByClass>(item.resource); + const instance = this.serviceProvider.getService>(item.resource); instance.markOutdated(undefined); }, }, @@ -182,12 +195,10 @@ export class PluginBootstrap extends Bootstrap { }); } - load(): void | Promise {} - - private getResources(providers: IServiceConstructor[]): ResourceSubMenuItem[] { - return providers - .filter(service => service.prototype instanceof CachedResource) - .sort((a, b) => a.name.localeCompare(b.name)) - .map(resource => new ResourceSubMenuItem(resource)); - } + // private getResources(providers: IServiceConstructor[]): ResourceSubMenuItem[] { + // return providers + // .filter(service => service.prototype instanceof CachedResource) + // .sort((a, b) => a.name.localeCompare(b.name)) + // .map(resource => new ResourceSubMenuItem(resource)); + // } } diff --git a/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS.ts b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS.ts index 59ed47c79a..a4b65e4010 100644 --- a/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS.ts +++ b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. 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', +}); diff --git a/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_OVERRIDE.ts b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_OVERRIDE.ts new file mode 100644 index 0000000000..ed89d2946a --- /dev/null +++ b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_OVERRIDE.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DEVTOOLS_OVERRIDE = createAction('devtools-override', { + type: 'checkbox', + label: 'Override', + tooltip: 'Override server settings', +}); diff --git a/webapp/packages/plugin-devtools/src/index.ts b/webapp/packages/plugin-devtools/src/index.ts index fd325378da..0345e62ba6 100644 --- a/webapp/packages/plugin-devtools/src/index.ts +++ b/webapp/packages/plugin-devtools/src/index.ts @@ -1,6 +1,15 @@ -import { devToolsPlugin } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { devToolsPlugin } from './manifest.js'; export { devToolsPlugin }; export default devToolsPlugin; -export * from './DevToolsService'; +export * from './DevToolsService.js'; diff --git a/webapp/packages/plugin-devtools/src/manifest.ts b/webapp/packages/plugin-devtools/src/manifest.ts index 8c8f433ec1..79bf2e70e1 100644 --- a/webapp/packages/plugin-devtools/src/manifest.ts +++ b/webapp/packages/plugin-devtools/src/manifest.ts @@ -1,18 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { DevToolsService } from './DevToolsService'; -import { PluginBootstrap } from './PluginBootstrap'; - export const devToolsPlugin: PluginManifest = { info: { name: 'DevTools plugin', }, - providers: [PluginBootstrap, DevToolsService], }; diff --git a/webapp/packages/plugin-devtools/src/menu/MENU_DEVTOOLS.ts b/webapp/packages/plugin-devtools/src/menu/MENU_DEVTOOLS.ts index d933a40386..89d98579cc 100644 --- a/webapp/packages/plugin-devtools/src/menu/MENU_DEVTOOLS.ts +++ b/webapp/packages/plugin-devtools/src/menu/MENU_DEVTOOLS.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const MENU_DEVTOOLS = createMenu('devtools', 'DevTools'); +export const MENU_DEVTOOLS = createMenu('devtools', { label: 'DevTools' }); diff --git a/webapp/packages/plugin-devtools/src/menu/MENU_PLUGIN.ts b/webapp/packages/plugin-devtools/src/menu/MENU_PLUGIN.ts index 9014e075ca..be0a4d076a 100644 --- a/webapp/packages/plugin-devtools/src/menu/MENU_PLUGIN.ts +++ b/webapp/packages/plugin-devtools/src/menu/MENU_PLUGIN.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const MENU_PLUGIN = createMenu('plugin', 'Plugin'); +export const MENU_PLUGIN = createMenu('plugin', { label: 'Plugin' }); diff --git a/webapp/packages/plugin-devtools/src/menu/MENU_PLUGINS.ts b/webapp/packages/plugin-devtools/src/menu/MENU_PLUGINS.ts index ed63a4c876..a3d308b33e 100644 --- a/webapp/packages/plugin-devtools/src/menu/MENU_PLUGINS.ts +++ b/webapp/packages/plugin-devtools/src/menu/MENU_PLUGINS.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const MENU_PLUGINS = createMenu('plugins', 'Plugins'); +export const MENU_PLUGINS = createMenu('plugins', { label: 'Plugins' }); diff --git a/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCE.ts b/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCE.ts index 4a95db6cde..0fa66f5993 100644 --- a/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCE.ts +++ b/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCE.ts @@ -1,10 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const MENU_RESOURCE = createMenu('resource', 'Resource', undefined, 'Resource actions'); +export const MENU_RESOURCE = createMenu('resource', { + label: 'Resource', + tooltip: 'Resource actions', +}); diff --git a/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCES.ts b/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCES.ts index 5dba7c317d..6207d006d0 100644 --- a/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCES.ts +++ b/webapp/packages/plugin-devtools/src/menu/MENU_RESOURCES.ts @@ -1,10 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const MENU_RESOURCES = createMenu('resources-list', 'Resources', undefined, 'List of registered resources'); +export const MENU_RESOURCES = createMenu('resources-list', { + label: 'Resources', + tooltip: 'List of registered resources', +}); diff --git a/webapp/packages/plugin-devtools/src/menu/PluginSubMenuItem.ts b/webapp/packages/plugin-devtools/src/menu/PluginSubMenuItem.ts index 5027ff9bb7..16bc269c24 100644 --- a/webapp/packages/plugin-devtools/src/menu/PluginSubMenuItem.ts +++ b/webapp/packages/plugin-devtools/src/menu/PluginSubMenuItem.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,7 +8,7 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { MenuSubMenuItem } from '@cloudbeaver/core-view'; -import { MENU_PLUGIN } from './MENU_PLUGIN'; +import { MENU_PLUGIN } from './MENU_PLUGIN.js'; export class PluginSubMenuItem extends MenuSubMenuItem { readonly plugin: PluginManifest; diff --git a/webapp/packages/plugin-devtools/src/menu/ResourceSubMenuItem.ts b/webapp/packages/plugin-devtools/src/menu/ResourceSubMenuItem.ts index 44f7b6482a..f39a3db117 100644 --- a/webapp/packages/plugin-devtools/src/menu/ResourceSubMenuItem.ts +++ b/webapp/packages/plugin-devtools/src/menu/ResourceSubMenuItem.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,7 +8,7 @@ import type { IServiceConstructor } from '@cloudbeaver/core-di'; import { MenuSubMenuItem } from '@cloudbeaver/core-view'; -import { MENU_RESOURCE } from './MENU_RESOURCE'; +import { MENU_RESOURCE } from './MENU_RESOURCE.js'; export class ResourceSubMenuItem extends MenuSubMenuItem { readonly resource: IServiceConstructor; diff --git a/webapp/packages/plugin-devtools/src/module.ts b/webapp/packages/plugin-devtools/src/module.ts new file mode 100644 index 0000000000..e4fb3a7264 --- /dev/null +++ b/webapp/packages/plugin-devtools/src/module.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ModuleRegistry, Bootstrap } from '@cloudbeaver/core-di'; +import { PluginBootstrap } from './PluginBootstrap.js'; +import { DevToolsService } from './DevToolsService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-devtools', + + configure: serviceCollection => { + serviceCollection.addSingleton(DevToolsService).addSingleton(Bootstrap, PluginBootstrap); + }, +}); diff --git a/webapp/packages/plugin-devtools/tsconfig.json b/webapp/packages/plugin-devtools/tsconfig.json index de53eafa9b..02f289d126 100644 --- a/webapp/packages/plugin-devtools/tsconfig.json +++ b/webapp/packages/plugin-devtools/tsconfig.json @@ -1,43 +1,44 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-settings-menu/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../plugin-user-profile/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-authentication/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-data-context/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-storage" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-settings/tsconfig.json" + "path": "../core-view" }, { - "path": "../core-ui/tsconfig.json" + "path": "../plugin-settings-menu" }, { - "path": "../core-view/tsconfig.json" + "path": "../plugin-user-profile" } ], "include": [ @@ -49,7 +50,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-gis-viewer/package.json b/webapp/packages/plugin-gis-viewer/package.json index e736c44641..b1aaf156ce 100644 --- a/webapp/packages/plugin-gis-viewer/package.json +++ b/webapp/packages/plugin-gis-viewer/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-gis-viewer", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,34 +11,41 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "leaflet": "~1.9.4", - "react-leaflet": "~4.2.1", - "wellknown": "~0.5.0", - "@cloudbeaver/plugin-data-viewer": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0" - }, - "peerDependencies": { - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "react": "~18.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/plugin-data-viewer": "workspace:*", + "leaflet": "^1", + "mobx": "^6", + "mobx-react-lite": "^4", + "proj4": "^2", + "react": "^19", + "react-dom": "^19", + "react-leaflet": "^5", + "tslib": "^2", + "wellknown": "^0" }, "devDependencies": { - "@types/leaflet": "^1.9.4", - "@types/react-leaflet": "~3.0.0", - "@types/wellknown": "~0.5.5" + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/leaflet": "^1", + "@types/proj4": "^2.19.0", + "@types/react": "^19", + "@types/wellknown": "^0", + "leaflet": "^1", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" } } diff --git a/webapp/packages/plugin-gis-viewer/src/CrsInput.module.css b/webapp/packages/plugin-gis-viewer/src/CrsInput.module.css new file mode 100644 index 0000000000..d67346fcca --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/CrsInput.module.css @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.root { + display: inline-flex; + align-items: center; + font-size: 12px; +} + +.combobox { + width: 120px; + flex: 0 0 auto; +} diff --git a/webapp/packages/plugin-gis-viewer/src/CrsInput.tsx b/webapp/packages/plugin-gis-viewer/src/CrsInput.tsx index 9ea822cff5..15e9f90179 100644 --- a/webapp/packages/plugin-gis-viewer/src/CrsInput.tsx +++ b/webapp/packages/plugin-gis-viewer/src/CrsInput.tsx @@ -1,40 +1,26 @@ -import styled, { css } from 'reshadow'; - -import { Combobox } from '@cloudbeaver/core-blocks'; - -import type { CrsKey } from './LeafletMap'; - -const styles = css` - root { - display: inline-flex; - align-items: center; - font-size: 12px; - } - - label { - margin-right: 4px; - flex-grow: 0; - flex-shrink: 1; - } - - Combobox { - width: 120px; - flex: 0 0 auto; - } -`; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Select } from '@cloudbeaver/core-blocks'; + +import classes from './CrsInput.module.css'; +import type { CrsKey } from './LeafletMap.js'; interface Props { value: CrsKey; onChange: (value: CrsKey) => void; } -const items: CrsKey[] = ['Simple', 'EPSG3395', 'EPSG3857', 'EPSG4326', 'EPSG900913']; +const items: CrsKey[] = ['Simple', 'EPSG:3395', 'EPSG:3857', 'EPSG:4326', 'EPSG:900913']; export function CrsInput(props: Props) { - return styled(styles)( - - - - , + return ( +
+ + ); + } + + return ( + + {name} + + ); +}); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/Settings.module.css b/webapp/packages/plugin-settings-panel/src/SettingsPanel/Settings.module.css new file mode 100644 index 0000000000..cb6b737d1b --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/Settings.module.css @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.settingsGroups { + width: 240px; +} + +.settingsGroups, +.settingsContainer { + height: 100%; +} diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/Settings.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/Settings.tsx new file mode 100644 index 0000000000..a45d217100 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/Settings.tsx @@ -0,0 +1,116 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { useId, useState } from 'react'; +import { observer } from 'mobx-react-lite'; + +import { Container, Filter, getComputed, Group, s, TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks'; +import { type IEditableSettingsSource, type ISettingsResolverSource, ROOT_SETTINGS_GROUP, SettingsGroup } from '@cloudbeaver/core-settings'; +import { useTreeData, useTreeFilter } from '@cloudbeaver/plugin-navigation-tree'; +import { SyncExecutor } from '@cloudbeaver/core-executor'; + +import classes from './Settings.module.css'; +import { settingsFilter } from './settingsFilter.js'; +import { SettingsGroups } from './SettingsGroups/SettingsGroups.js'; +import { SettingsList } from './SettingsList.js'; +import { useSettings } from './useSettings.js'; + +export interface ISettingsProps { + resolver: ISettingsResolverSource; + source: IEditableSettingsSource; + accessor?: string[]; + hideGroupsSettingsLimit?: number; + displayRestore?: boolean; +} + +export const Settings = observer(function Settings({ resolver, source, accessor, hideGroupsSettingsLimit = 0, displayRestore }) { + const translate = useTranslate(); + const settingsId = useId(); + const settings = useSettings(accessor); + const [groupSelectExecutor] = useState(() => new SyncExecutor()); + + function filterExistsGroups(group: SettingsGroup) { + return settings.groups.has(group); + } + + const treeFilter = useTreeFilter({ + isNodeMatched(nodeId, filter) { + const group = ROOT_SETTINGS_GROUP.get(nodeId)!; + const groupSettings = settings.settings.get(group); + + if (!groupSettings) { + return false; + } + + return groupSettings.some(settingsFilter(translate, filter)); + }, + }); + + const treeData = useTreeData({ + rootId: ROOT_SETTINGS_GROUP.id, + childrenTransformers: [treeFilter.transformer], + stateTransformers: [treeFilter.stateTransformer], + getNode(id) { + const group = ROOT_SETTINGS_GROUP.get(id); + + return { + name: translate(group!.name), + leaf: !group?.subGroups.filter(filterExistsGroups).length, + }; + }, + getChildren(id) { + return (ROOT_SETTINGS_GROUP.get(id)?.subGroups || []) + .filter(filterExistsGroups) + .sort((a, b) => a.order - b.order) + .map(group => group.id); + }, + load() { + return Promise.resolve(); + }, + }); + + if (settings.settings.size === 0) { + return {translate('plugin_settings_panel_empty')}; + } + + function handleClick(id: string) { + groupSelectExecutor.execute(id); + } + + const isGroupsHidden = getComputed(() => [...settings.settings.values()].flat().length <= hideGroupsSettingsLimit); + + return ( + + + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroup.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroup.tsx index 82977bcdcf..828d71696b 100644 --- a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroup.tsx +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroup.tsx @@ -1,54 +1,83 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 React from 'react'; -import { Group, GroupTitle, useTranslate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { PluginManagerService } from '@cloudbeaver/core-plugin'; -import { SettingsGroupType, SettingsManagerService, SettingsScopeType } from '@cloudbeaver/core-settings'; +import { getComputed, Group, GroupTitle, useExecutor, useTranslate } from '@cloudbeaver/core-blocks'; +import type { + IEditableSettingsSource, + ISettingDescription, + ISettingsResolverSource, + SettingsGroup as SettingsGroupType, +} from '@cloudbeaver/core-settings'; +import { isArraysEqual } from '@cloudbeaver/core-utils'; +import type { ITreeFilter } from '@cloudbeaver/plugin-navigation-tree'; -import { SettingsInfoForm } from './SettingsInfoForm'; +import { Setting } from './Setting.js'; +import { settingsFilter } from './settingsFilter.js'; +import { SettingsGroupTitle } from './SettingsGroupTitle.js'; +import type { ISyncExecutor } from '@cloudbeaver/core-executor'; +import { useRef } from 'react'; +import { getSettingGroupId } from './getSettingGroupId.js'; interface Props { + settingsId: string; group: SettingsGroupType; + resolver: ISettingsResolverSource; + source: IEditableSettingsSource; + settings: Map[]>; + treeFilter: ITreeFilter; + groupsHidden?: boolean; + displayRestore?: boolean; + groupSelectExecutor: ISyncExecutor; } -export const SettingsGroup = observer(function SettingsGroup({ group }) { - const settingsManagerService = useService(SettingsManagerService); - const pluginManagerService = useService(PluginManagerService); +export const SettingsGroup = observer(function SettingsGroup({ + settingsId, + group, + resolver, + source, + settings, + treeFilter, + groupsHidden, + displayRestore, + groupSelectExecutor, +}) { + const ref = useRef(null); const translate = useTranslate(); + const groupSettings = getComputed(() => settings.get(group)?.filter(settingsFilter(translate, treeFilter.filter)) || [], isArraysEqual); + const hidden = groupSettings.length === 0; - function getValue(scope: string, scopeType: SettingsScopeType, key: string) { - const settings = pluginManagerService.getSettings(scope, scopeType); - return settings?.getValue(key); - } - - const settings = settingsManagerService.settings - .filter(settingsItem => settingsItem.groupId === group.id) - .map(settingsItem => ({ - ...settingsItem, - value: getValue(settingsItem.scope, settingsItem.scopeType, settingsItem.key), - name: translate(settingsItem.name), - description: translate(settingsItem.description), - options: settingsItem.options?.map(option => ({ ...option, name: translate(option.name) })), - })); - - if (settings.length === 0) { - return null; - } + useExecutor({ + executor: groupSelectExecutor, + handlers: [ + id => { + if (id === group.id) { + if (hidden) { + const next = ref.current?.nextSibling; + if (next instanceof HTMLElement) { + next?.scrollIntoView(); + } + } else { + ref.current?.scrollIntoView(); + } + } + }, + ], + }); return ( - - - {translate(group.name)} + ); }); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.module.css b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.module.css new file mode 100644 index 0000000000..80f8613c18 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.module.css @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.icon { + transform: rotate(-90deg); + height: 16px; + width: 16px; +} + +.box { + display: flex; + align-items: center; + gap: 4px; +} diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.tsx new file mode 100644 index 0000000000..07ad968996 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.tsx @@ -0,0 +1,33 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Icon, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { ROOT_SETTINGS_GROUP, type SettingsGroup } from '@cloudbeaver/core-settings'; + +import style from './SettingsGroupTitle.module.css'; + +interface Props { + group: SettingsGroup; +} + +export const SettingsGroupTitle = observer(function SettingsGroupTitle({ group }) { + const translate = useTranslate(); + const styles = useS(style); + return ( +
+ {group.parent && group.parent !== ROOT_SETTINGS_GROUP && ( + <> + + + + )} +
{translate(group.name)}
+
+ ); +}); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/GroupNodeControl.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/GroupNodeControl.tsx new file mode 100644 index 0000000000..b711b3953c --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/GroupNodeControl.tsx @@ -0,0 +1,29 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import { forwardRef, useContext } from 'react'; + +import { TreeNodeControl, TreeNodeExpand, TreeNodeName } from '@cloudbeaver/core-blocks'; +import { type NodeControlComponent, TreeContext, TreeDataContext } from '@cloudbeaver/plugin-navigation-tree'; + +export const GroupNodeControl: NodeControlComponent = observer( + forwardRef(function GroupNodeControl({ nodeId }, ref) { + const data = useContext(TreeDataContext)!; + const tree = useContext(TreeContext)!; + + const node = data.getNode(nodeId); + const height = tree.getNodeHeight(nodeId); + + return ( + + + {node.name} + + ); + }), +); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/SettingsGroups.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/SettingsGroups.tsx new file mode 100644 index 0000000000..267bfb82bc --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/SettingsGroups.tsx @@ -0,0 +1,34 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { type ITreeData, Tree } from '@cloudbeaver/plugin-navigation-tree'; + +import { groupNodeRenderer } from './groupNodeRenderer.js'; +import { SettingsGroupsEmpty } from './SettingsGroupsEmpty.js'; + +interface Props { + treeData: ITreeData; + onClick?: (groupId: string) => void; +} + +export const SettingsGroups = observer(function SettingsGroups({ treeData, onClick }) { + function getNodeHeight(id: string) { + return 24; + } + + return ( + + ); +}); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/SettingsGroupsEmpty.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/SettingsGroupsEmpty.tsx new file mode 100644 index 0000000000..71f56d3568 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/SettingsGroupsEmpty.tsx @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Translate, TreeNodeNestedMessage } from '@cloudbeaver/core-blocks'; +import type { NodeEmptyPlaceholderComponent } from '@cloudbeaver/plugin-navigation-tree'; + +export const SettingsGroupsEmpty: NodeEmptyPlaceholderComponent = function SettingsGroupsEmpty({ root }) { + return ( + + + + ); +}; diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/groupNodeRenderer.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/groupNodeRenderer.tsx new file mode 100644 index 0000000000..8fbc06d87b --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/groupNodeRenderer.tsx @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { type INodeRenderer, Node, type NodeComponent } from '@cloudbeaver/plugin-navigation-tree'; + +import { GroupNodeControl } from './GroupNodeControl.js'; + +export const groupNodeRenderer: INodeRenderer = () => GroupNodeRenderer; + +const GroupNodeRenderer: NodeComponent = function GroupNodeRenderer(props) { + return ; +}; diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsInfoForm.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsInfoForm.tsx deleted file mode 100644 index b1686b395f..0000000000 --- a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsInfoForm.tsx +++ /dev/null @@ -1,60 +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 { observer } from 'mobx-react-lite'; - -import { TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks'; -import type { FormFieldType } from '@cloudbeaver/core-settings'; - -import { SettingsInfoFormField } from './SettingsInfoFormField'; - -interface SettingsProps { - key: string; - type: FormFieldType; - disabled?: boolean; - value?: any; - options?: any[]; - description?: string; - name?: string; -} - -interface SettingsInfoFormProps { - fields: SettingsProps[]; - className?: string; - readOnly?: boolean; - onSelect?: (value: any) => void; - onChange?: (value: any) => void; -} - -export const SettingsInfoForm = observer(function SettingsInfoForm({ fields, className, readOnly, onChange, onSelect }) { - const translate = useTranslate(); - - if (fields.length === 0) { - return {translate('settings_panel_empty_fields')}; - } - - return ( - <> - {fields.map(field => ( - - ))} - - ); -}); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsInfoFormField.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsInfoFormField.tsx deleted file mode 100644 index ecfc52c340..0000000000 --- a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsInfoFormField.tsx +++ /dev/null @@ -1,104 +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 { observer } from 'mobx-react-lite'; - -import { Combobox, FieldCheckbox, InputField, Textarea } from '@cloudbeaver/core-blocks'; -import { FormFieldType } from '@cloudbeaver/core-settings'; - -export interface SettingsInfoFormFieldProps { - id: string; - type: FormFieldType; - disabled?: boolean; - value?: any; - options?: any[]; - description?: string; - name?: string; - className?: string; - readOnly?: boolean; - onSelect?: (value: any) => void; - onChange?: (value: any) => void; -} - -export const SettingsInfoFormField = observer(function SettingsInfoFormField({ - id, - type, - value, - description, - disabled, - name, - className, - readOnly, - options, - onSelect, - onChange, -}) { - if (type === FormFieldType.Checkbox) { - return ( - - {name ?? ''} - - ); - } - - if (type === FormFieldType.Combobox && options !== undefined) { - return ( - value.id} - valueSelector={value => value.name} - value={value} - title={description} - disabled={disabled} - readOnly={readOnly} - className={className} - tiny - onSelect={onSelect} - > - {name ?? ''} - - ); - } - - if (type === FormFieldType.Textarea) { - return ( - - ); - } - - return ( - - {name ?? ''} - - ); -}); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsLazy.ts b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsLazy.ts new file mode 100644 index 0000000000..2b6e4744af --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsLazy.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const Settings = importLazyComponent(() => import('./Settings.js').then(m => m.Settings)); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsList.module.css b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsList.module.css new file mode 100644 index 0000000000..3e2692a03c --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsList.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.spaceFill { + height: 25%; +} diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsList.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsList.tsx new file mode 100644 index 0000000000..c1ea691f21 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsList.tsx @@ -0,0 +1,74 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Group, s, TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks'; +import { + type IEditableSettingsSource, + type ISettingDescription, + type ISettingsResolverSource, + type SettingsGroup as SettingsGroupType, +} from '@cloudbeaver/core-settings'; +import type { ITreeData, ITreeFilter } from '@cloudbeaver/plugin-navigation-tree'; + +import { getGroupsFromTree } from './getGroupsFromTree.js'; +import { SettingsGroup } from './SettingsGroup.js'; +import classes from './SettingsList.module.css'; +import { useTreeScrollSync } from './useTreeScrollSync.js'; +import type { ISyncExecutor } from '@cloudbeaver/core-executor'; + +interface Props { + settingsId: string; + treeData: ITreeData; + treeFilter: ITreeFilter; + source: IEditableSettingsSource; + resolver: ISettingsResolverSource; + settings: Map[]>; + groupSelectExecutor: ISyncExecutor; + groupsHidden?: boolean; + displayRestore?: boolean; + onSettingsOpen?: (groupId: string) => void; +} + +export const SettingsList = observer(function SettingsList({ + settingsId, + treeData, + treeFilter, + resolver, + source, + settings, + groupsHidden, + displayRestore, + groupSelectExecutor, + onSettingsOpen, +}) { + const translate = useTranslate(); + const ref = useTreeScrollSync(settingsId, treeData, onSettingsOpen); + const groups = Array.from(getGroupsFromTree(treeData, treeData.getChildren(treeData.rootId))); + + return ( + + {groups.map(group => ( + + ))} + {groups.length === 0 && {translate('plugin_settings_panel_no_settings')}} +
+ + ); +}); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsPanelForm.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsPanelForm.tsx deleted file mode 100644 index 6b258f6a51..0000000000 --- a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsPanelForm.tsx +++ /dev/null @@ -1,44 +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 { observer } from 'mobx-react-lite'; -import React from 'react'; -import styled, { css } from 'reshadow'; - -import { ColoredContainer, Container, useStyles } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { SettingsManagerService } from '@cloudbeaver/core-settings'; - -import { SettingsGroup } from './SettingsGroup'; - -const styles = css` - content { - height: 100%; - display: flex; - flex-direction: column; - overflow: auto; - } -`; - -export const SettingsPanelForm = observer(function SettingsPanelForm() { - const style = useStyles(styles); - - const settingsManagerService = useService(SettingsManagerService); - const groups = Array.from(settingsManagerService.groups.values()); - - return styled(style)( - - - - {groups.map(group => ( - - ))} - - - , - ); -}); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/getGroupsFromTree.ts b/webapp/packages/plugin-settings-panel/src/SettingsPanel/getGroupsFromTree.ts new file mode 100644 index 0000000000..4ae28e709f --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/getGroupsFromTree.ts @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { ROOT_SETTINGS_GROUP, type SettingsGroup as SettingsGroupType } from '@cloudbeaver/core-settings'; +import type { ITreeData } from '@cloudbeaver/plugin-navigation-tree'; + +export function* getGroupsFromTree(treeData: ITreeData, groups: string[]): IterableIterator { + for (const groupId of groups) { + const group = ROOT_SETTINGS_GROUP.get(groupId); + + if (group) { + yield group; + } + + yield* getGroupsFromTree(treeData, treeData.getChildren(groupId)); + } +} diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/getSettingGroupId.ts b/webapp/packages/plugin-settings-panel/src/SettingsPanel/getSettingGroupId.ts new file mode 100644 index 0000000000..c3546d34a8 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/getSettingGroupId.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { SETTINGS_GROUP_ID_PREFIX } from './SETTINGS_GROUP_ID_PREFIX.js'; + +export function getSettingGroupId(settingsId: string, id: string): string { + return `${SETTINGS_GROUP_ID_PREFIX}${settingsId}-${id}`; +} diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/getSettingGroupIdFromElementId.ts b/webapp/packages/plugin-settings-panel/src/SettingsPanel/getSettingGroupIdFromElementId.ts new file mode 100644 index 0000000000..8ea08ef9a7 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/getSettingGroupIdFromElementId.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { SETTINGS_GROUP_ID_PREFIX } from './SETTINGS_GROUP_ID_PREFIX.js'; + +export function getSettingGroupIdFromElementId(settingsId: string, id: string): string { + return id.replace(SETTINGS_GROUP_ID_PREFIX + `${settingsId}-`, ''); +} diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/querySettingsGroups.ts b/webapp/packages/plugin-settings-panel/src/SettingsPanel/querySettingsGroups.ts new file mode 100644 index 0000000000..7141aac911 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/querySettingsGroups.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { SETTINGS_GROUP_ID_PREFIX } from './SETTINGS_GROUP_ID_PREFIX.js'; + +export function querySettingsGroups(element: HTMLElement): HTMLElement[] { + return Array.from(element.querySelectorAll(`[id^="${SETTINGS_GROUP_ID_PREFIX}"]`)); +} diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/settingsFilter.ts b/webapp/packages/plugin-settings-panel/src/SettingsPanel/settingsFilter.ts new file mode 100644 index 0000000000..2126ee49e5 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/settingsFilter.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { TranslateFn } from '@cloudbeaver/core-localization'; +import type { ISettingDescription } from '@cloudbeaver/core-settings'; + +export function settingsFilter(translate: TranslateFn, filter: string) { + filter = filter.trim(); + return (setting: ISettingDescription) => + translate(setting.name).toLowerCase().includes(filter.toLowerCase()) || + translate(setting.description)?.toLowerCase().includes(filter.toLowerCase()); +} diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/useSettings.ts b/webapp/packages/plugin-settings-panel/src/SettingsPanel/useSettings.ts new file mode 100644 index 0000000000..170794ba21 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/useSettings.ts @@ -0,0 +1,61 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, observable } from 'mobx'; + +import { useAutoLoad, useObservableRef } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { type ISettingDescription, ROOT_SETTINGS_GROUP, SettingsGroup, SettingsManagerService } from '@cloudbeaver/core-settings'; + +interface ISettings { + settings: Map[]>; + groups: Set; +} + +export function useSettings(accessor?: string[]): ISettings { + const settingsManagerService = useService(SettingsManagerService); + + useAutoLoad(useSettings, settingsManagerService.loaders); + + return useObservableRef( + () => ({ + get settings() { + const map = new Map(); + const settings = this.settingsManagerService.activeSettings + .filter(setting => this.accessor?.some(value => setting.access.scope.includes(value))) + .sort((a, b) => a.name.localeCompare(b.name)); + + for (const setting of settings) { + map.set(setting.group, [...(map.get(setting.group) || []), setting]); + } + + return map; + }, + get groups() { + const groups = new Set(this.settings.keys()); + + for (const group of groups) { + if (group.parent) { + groups.add(group.parent); + } + } + + return groups; + }, + groupChildren(id: string) { + return (ROOT_SETTINGS_GROUP.get(id)?.subGroups || []).filter(group => this.groups.has(group)).sort((a, b) => a.name.localeCompare(b.name)); + }, + }), + { + settings: computed, + groups: computed, + settingsManagerService: observable.ref, + accessor: observable.ref, + }, + { settingsManagerService, accessor }, + ); +} diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/useTreeScrollSync.ts b/webapp/packages/plugin-settings-panel/src/SettingsPanel/useTreeScrollSync.ts new file mode 100644 index 0000000000..43f1856e0d --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/useTreeScrollSync.ts @@ -0,0 +1,76 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { useEffect, useRef } from 'react'; + +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; +import { throttle } from '@cloudbeaver/core-utils'; +import type { ITreeData } from '@cloudbeaver/plugin-navigation-tree'; + +import { getSettingGroupIdFromElementId } from './getSettingGroupIdFromElementId.js'; +import { querySettingsGroups } from './querySettingsGroups.js'; + +export function useTreeScrollSync( + settingsId: string, + treeData: ITreeData, + onSettingsOpen?: (groupId: string) => void, +): React.RefObject { + const ref = useRef(null); + + useEffect(() => { + const element = ref.current; + + if (!element) { + return; + } + + const syncScroll = throttle(function syncScroll(e: { target: Element | EventTarget | null }) { + const container = e.target as HTMLDivElement; + + const elements = querySettingsGroups(container); + let firstVisibleElement: HTMLElement | undefined; + + for (const element of elements) { + if (element.offsetTop + element.offsetHeight > container.scrollTop) { + firstVisibleElement = element; + break; + } + } + + if (firstVisibleElement) { + const groupId = getSettingGroupIdFromElementId(settingsId, firstVisibleElement.id); + let group = ROOT_SETTINGS_GROUP.get(groupId)!; + + treeData.updateAllState({ selected: false, expanded: false }); + treeData.updateState(groupId, { selected: true }); + + while (group.parent && group.parent !== ROOT_SETTINGS_GROUP) { + treeData.updateState(group.parent.id, { expanded: true }); + group = group.parent; + } + onSettingsOpen?.(groupId); + } + }, 50); + + const resizeObserver = new ResizeObserver(entries => { + for (const entry of entries) { + syncScroll({ target: entry.target }); + } + }); + + resizeObserver.observe(element); + + element.addEventListener('scroll', syncScroll); + + return () => { + resizeObserver.disconnect(); + element.removeEventListener('scroll', syncScroll); + }; + }); + + return ref; +} diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanelPluginBootstrap.ts b/webapp/packages/plugin-settings-panel/src/SettingsPanelPluginBootstrap.ts index e2ead9b808..e5fb56c168 100644 --- a/webapp/packages/plugin-settings-panel/src/SettingsPanelPluginBootstrap.ts +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanelPluginBootstrap.ts @@ -1,46 +1,15 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { AuthInfoService } from '@cloudbeaver/core-authentication'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { OptionsPanelService } from '@cloudbeaver/core-ui'; -import { ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view'; -import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu'; - -import { ACTION_OPEN_APP_SETTINGS } from './actions/ACTION_OPEN_APP_SETTINGS'; -import { SettingsPanelForm } from './SettingsPanel/SettingsPanelForm'; @injectable() export class SettingsPanelPluginBootstrap extends Bootstrap { - constructor( - private readonly menuService: MenuService, - private readonly optionsPanelService: OptionsPanelService, - private readonly authInfoService: AuthInfoService, - private readonly actionService: ActionService, - ) { + constructor() { super(); } - - register(): void | Promise { - this.addTopAppMenuItems(); - } - - load(): void | Promise {} - - private addTopAppMenuItems() { - this.actionService.addHandler({ - id: 'open-app-settings', - isActionApplicable: (context, action) => ACTION_OPEN_APP_SETTINGS === action, - handler: () => this.optionsPanelService.open(() => SettingsPanelForm), - }); - - this.menuService.addCreator({ - isApplicable: context => context.get(DATA_CONTEXT_MENU) === TOP_NAV_BAR_SETTINGS_MENU && !!this.authInfoService.userInfo, - getItems: (context, items) => [...items, ACTION_OPEN_APP_SETTINGS], - }); - } } diff --git a/webapp/packages/plugin-settings-panel/src/actions/ACTION_OPEN_APP_SETTINGS.ts b/webapp/packages/plugin-settings-panel/src/actions/ACTION_OPEN_APP_SETTINGS.ts deleted file mode 100644 index a1c571f2bf..0000000000 --- a/webapp/packages/plugin-settings-panel/src/actions/ACTION_OPEN_APP_SETTINGS.ts +++ /dev/null @@ -1,13 +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 { createAction } from '@cloudbeaver/core-view'; - -export const ACTION_OPEN_APP_SETTINGS = createAction('open-app-settings', { - label: 'settings_panel', - tooltip: 'settings_panel', -}); diff --git a/webapp/packages/plugin-settings-panel/src/index.ts b/webapp/packages/plugin-settings-panel/src/index.ts index 95d27ab942..00aa527e51 100644 --- a/webapp/packages/plugin-settings-panel/src/index.ts +++ b/webapp/packages/plugin-settings-panel/src/index.ts @@ -1,6 +1,13 @@ -import { settingsPanelPlugin } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 * from './SettingsPanel/SettingsInfoFormField'; -export * from './SettingsPanel/SettingsInfoForm'; +import './module.js'; +import { settingsPanelPlugin } from './manifest.js'; +export * from './SettingsPanel/SettingsLazy.js'; export default settingsPanelPlugin; diff --git a/webapp/packages/plugin-settings-panel/src/locales/en.ts b/webapp/packages/plugin-settings-panel/src/locales/en.ts index 922344e12d..75769b659d 100644 --- a/webapp/packages/plugin-settings-panel/src/locales/en.ts +++ b/webapp/packages/plugin-settings-panel/src/locales/en.ts @@ -1,4 +1,9 @@ export default [ - ['settings_panel', 'Settings'], - ['settings_panel_empty_fields', 'Fields are empty'], + ['plugin_settings_panel_empty', 'No settings available'], + ['plugin_settings_panel_search', 'Search settings...'], + ['plugin_settings_panel_no_settings', 'No settings found'], + ['plugin_settings_panel_group_empty', 'No settings available'], + ['plugin_settings_panel_setting_reset', 'Reset'], + ['plugin_settings_panel_setting_reset_tooltip', 'Reset to default'], + ['plugin_settings_panel_setting_set_in_scope', 'The setting has been configured in the current scope.'], ]; diff --git a/webapp/packages/plugin-settings-panel/src/locales/fr.ts b/webapp/packages/plugin-settings-panel/src/locales/fr.ts new file mode 100644 index 0000000000..f2c5ed7d2d --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/locales/fr.ts @@ -0,0 +1,9 @@ +export default [ + ['plugin_settings_panel_empty', 'Aucun paramètre disponible'], + ['plugin_settings_panel_search', 'Search settings...'], + ['plugin_settings_panel_no_settings', 'No settings found'], + ['plugin_settings_panel_group_empty', 'No settings available'], + ['plugin_settings_panel_setting_reset', 'Reset'], + ['plugin_settings_panel_setting_reset_tooltip', 'Reset to default'], + ['plugin_settings_panel_setting_set_in_scope', 'The setting has been configured in the current scope.'], +]; diff --git a/webapp/packages/plugin-settings-panel/src/locales/it.ts b/webapp/packages/plugin-settings-panel/src/locales/it.ts index 922344e12d..75769b659d 100644 --- a/webapp/packages/plugin-settings-panel/src/locales/it.ts +++ b/webapp/packages/plugin-settings-panel/src/locales/it.ts @@ -1,4 +1,9 @@ export default [ - ['settings_panel', 'Settings'], - ['settings_panel_empty_fields', 'Fields are empty'], + ['plugin_settings_panel_empty', 'No settings available'], + ['plugin_settings_panel_search', 'Search settings...'], + ['plugin_settings_panel_no_settings', 'No settings found'], + ['plugin_settings_panel_group_empty', 'No settings available'], + ['plugin_settings_panel_setting_reset', 'Reset'], + ['plugin_settings_panel_setting_reset_tooltip', 'Reset to default'], + ['plugin_settings_panel_setting_set_in_scope', 'The setting has been configured in the current scope.'], ]; diff --git a/webapp/packages/plugin-settings-panel/src/locales/ru.ts b/webapp/packages/plugin-settings-panel/src/locales/ru.ts index 293d6f8c8c..bfccb4ecbe 100644 --- a/webapp/packages/plugin-settings-panel/src/locales/ru.ts +++ b/webapp/packages/plugin-settings-panel/src/locales/ru.ts @@ -1,4 +1,9 @@ export default [ - ['settings_panel', 'Настройки'], - ['settings_panel_empty_fields', 'Поля пустые'], + ['plugin_settings_panel_empty', 'Настройки недоступны'], + ['plugin_settings_panel_search', 'Поиск настроек...'], + ['plugin_settings_panel_no_settings', 'Настройки не найдены'], + ['plugin_settings_panel_group_empty', 'Настройки недоступны'], + ['plugin_settings_panel_setting_reset', 'Сбросить'], + ['plugin_settings_panel_setting_reset_tooltip', 'Сбросить на значение по умолчанию'], + ['plugin_settings_panel_setting_set_in_scope', 'Настройка установлена в текущей области.'], ]; diff --git a/webapp/packages/plugin-settings-panel/src/locales/vi.ts b/webapp/packages/plugin-settings-panel/src/locales/vi.ts new file mode 100644 index 0000000000..67b8116746 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/locales/vi.ts @@ -0,0 +1,6 @@ +export default [ + ['plugin_settings_panel_empty', 'Không có cài đặt nào'], + ['plugin_settings_panel_search', 'Tìm kiếm cài đặt...'], + ['plugin_settings_panel_no_settings', 'Không tìm thấy cài đặt nào'], + ['plugin_settings_panel_group_empty', 'Không có cài đặt nào'], +]; diff --git a/webapp/packages/plugin-settings-panel/src/locales/zh.ts b/webapp/packages/plugin-settings-panel/src/locales/zh.ts index 922344e12d..b3d25f5a7a 100644 --- a/webapp/packages/plugin-settings-panel/src/locales/zh.ts +++ b/webapp/packages/plugin-settings-panel/src/locales/zh.ts @@ -1,4 +1,9 @@ export default [ - ['settings_panel', 'Settings'], - ['settings_panel_empty_fields', 'Fields are empty'], + ['plugin_settings_panel_empty', '无可用设置'], + ['plugin_settings_panel_search', 'Search settings...'], + ['plugin_settings_panel_no_settings', 'No settings found'], + ['plugin_settings_panel_group_empty', 'No settings available'], + ['plugin_settings_panel_setting_reset', 'Reset'], + ['plugin_settings_panel_setting_reset_tooltip', 'Reset to default'], + ['plugin_settings_panel_setting_set_in_scope', 'The setting has been configured in the current scope.'], ]; diff --git a/webapp/packages/plugin-settings-panel/src/manifest.ts b/webapp/packages/plugin-settings-panel/src/manifest.ts index d41a04865d..03ef9dc094 100644 --- a/webapp/packages/plugin-settings-panel/src/manifest.ts +++ b/webapp/packages/plugin-settings-panel/src/manifest.ts @@ -1,16 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { LocaleService } from './LocaleService'; -import { SettingsPanelPluginBootstrap } from './SettingsPanelPluginBootstrap'; - export const settingsPanelPlugin: PluginManifest = { info: { name: 'Settings panel plugin' }, - providers: [SettingsPanelPluginBootstrap, LocaleService], }; diff --git a/webapp/packages/plugin-settings-panel/src/module.ts b/webapp/packages/plugin-settings-panel/src/module.ts new file mode 100644 index 0000000000..854c72b6f1 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/module.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, ModuleRegistry } from '@cloudbeaver/core-di'; +import { SettingsPanelPluginBootstrap } from './SettingsPanelPluginBootstrap.js'; +import { LocaleService } from './LocaleService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-settings-panel', + + configure: serviceCollection => { + serviceCollection.addSingleton(Bootstrap, LocaleService).addSingleton(Bootstrap, SettingsPanelPluginBootstrap); + }, +}); diff --git a/webapp/packages/plugin-settings-panel/tsconfig.json b/webapp/packages/plugin-settings-panel/tsconfig.json index 50d49256ff..880e735f7f 100644 --- a/webapp/packages/plugin-settings-panel/tsconfig.json +++ b/webapp/packages/plugin-settings-panel/tsconfig.json @@ -1,37 +1,44 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-settings-menu/tsconfig.json" + "path": "../../common-react/@dbeaver/ui-kit" }, { - "path": "../core-authentication/tsconfig.json" + "path": "../../common-typescript/@dbeaver/cli" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../../common-typescript/@dbeaver/js-helpers" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-settings/tsconfig.json" + "path": "../core-executor" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-view/tsconfig.json" + "path": "../core-settings" + }, + { + "path": "../core-utils" + }, + { + "path": "../plugin-navigation-tree" } ], "include": [ @@ -43,7 +50,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/package.json b/webapp/packages/plugin-sql-editor-navigation-tab-script/package.json index 0673f29dc8..10ac7a2992 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/package.json +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-sql-editor-navigation-tab-script", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,43 +11,46 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-navigation-tabs": "~0.1.0", - "@cloudbeaver/plugin-navigation-tree-rm": "~0.1.0", - "@cloudbeaver/plugin-resource-manager": "~0.1.0", - "@cloudbeaver/plugin-resource-manager-scripts": "~0.1.0", - "@cloudbeaver/plugin-sql-editor": "~0.1.0", - "@cloudbeaver/plugin-sql-editor-navigation-tab": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-projects": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-resource-manager": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-navigation-tree": "workspace:*", + "@cloudbeaver/core-projects": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-resource-manager": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-settings": "workspace:*", + "@cloudbeaver/core-storage": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-navigation-tabs": "workspace:*", + "@cloudbeaver/plugin-navigation-tree-rm": "workspace:*", + "@cloudbeaver/plugin-resource-manager": "workspace:*", + "@cloudbeaver/plugin-resource-manager-scripts": "workspace:*", + "@cloudbeaver/plugin-sql-editor": "workspace:*", + "@cloudbeaver/plugin-sql-editor-navigation-tab": "workspace:*", + "@dbeaver/js-helpers": "workspace:^", + "mobx": "^6", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "typescript": "^5" + } } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ACTION_SAVE_AS_SCRIPT.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ACTION_SAVE_AS_SCRIPT.ts index cae6f62deb..611957e2dc 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ACTION_SAVE_AS_SCRIPT.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ACTION_SAVE_AS_SCRIPT.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,5 +10,5 @@ import { createAction } from '@cloudbeaver/core-view'; export const ACTION_SAVE_AS_SCRIPT = createAction('save-as-script', { label: 'plugin_sql_editor_navigation_tab_resource_save_script_title', tooltip: 'plugin_sql_editor_navigation_tab_resource_save_script_title', - icon: '/icons/sql_script_sm.svg', + icon: '/icons/save.svg', }); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/IResourceSqlDataSourceState.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/IResourceSqlDataSourceState.ts index 1bf31f108c..58ab5ac455 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/IResourceSqlDataSourceState.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/IResourceSqlDataSourceState.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT.ts new file mode 100644 index 0000000000..54eab7c52d --- /dev/null +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createKeyBinding } from '@cloudbeaver/core-view'; + +export const KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT = createKeyBinding({ + id: 'save-as-script', + keys: 'shift+mod+s', + preventDefault: true, +}); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/LocaleService.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/LocaleService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts index f778fd15ab..d98581a2df 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts @@ -1,10 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { IDataContextProvider } from '@cloudbeaver/core-data-context'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -14,7 +15,7 @@ import { isResourceOfType, ProjectInfoResource, ProjectsService } from '@cloudbe import { CachedMapAllKey, CachedTreeChildrenKey } from '@cloudbeaver/core-resource'; import { getRmResourcePath, NAV_NODE_TYPE_RM_RESOURCE, ResourceManagerResource, RESOURCES_NODE_PATH } from '@cloudbeaver/core-resource-manager'; import { createPath, getPathName } from '@cloudbeaver/core-utils'; -import { ACTION_SAVE, ActionService, DATA_CONTEXT_MENU, KEY_BINDING_SAVE, KeyBindingService, MenuService } from '@cloudbeaver/core-view'; +import { ActionService, KeyBindingService, MenuService } from '@cloudbeaver/core-view'; import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; import { getResourceKeyFromNodeId } from '@cloudbeaver/plugin-navigation-tree-rm'; import { RESOURCE_NAME_REGEX, ResourceManagerService } from '@cloudbeaver/plugin-resource-manager'; @@ -23,20 +24,41 @@ import { DATA_CONTEXT_SQL_EDITOR_STATE, ESqlDataSourceFeatures, getSqlEditorName, - ISqlDataSource, + type ISqlDataSource, LocalStorageSqlDataSource, MemorySqlDataSource, SQL_EDITOR_TOOLS_MENU, SqlDataSourceService, SqlEditorSettingsService, + SqlEditorView, } from '@cloudbeaver/plugin-sql-editor'; import { isSQLEditorTab, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab'; -import { ACTION_SAVE_AS_SCRIPT } from './ACTION_SAVE_AS_SCRIPT'; -import { ResourceSqlDataSource } from './ResourceSqlDataSource'; -import { SqlEditorTabResourceService } from './SqlEditorTabResourceService'; - -@injectable() +import { ACTION_SAVE_AS_SCRIPT } from './ACTION_SAVE_AS_SCRIPT.js'; +import { KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT } from './KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT.js'; +import { ResourceSqlDataSource } from './ResourceSqlDataSource.js'; +import { SqlEditorTabResourceService } from './SqlEditorTabResourceService.js'; + +@injectable(() => [ + NavNodeManagerService, + NavNodeInfoResource, + NavigationTabsService, + NotificationService, + SqlEditorNavigatorService, + ResourceManagerService, + ProjectsService, + ProjectInfoResource, + SqlEditorTabResourceService, + CommonDialogService, + ActionService, + MenuService, + SqlDataSourceService, + SqlEditorSettingsService, + ResourceManagerResource, + ResourceManagerScriptsService, + KeyBindingService, + SqlEditorView, +]) export class PluginBootstrap extends Bootstrap { constructor( private readonly navNodeManagerService: NavNodeManagerService, @@ -56,22 +78,22 @@ export class PluginBootstrap extends Bootstrap { private readonly resourceManagerResource: ResourceManagerResource, private readonly resourceManagerScriptsService: ResourceManagerScriptsService, private readonly keyBindingService: KeyBindingService, + private readonly sqlEditorView: SqlEditorView, ) { super(); + this.saveAsScriptHandler = this.saveAsScriptHandler.bind(this); } - register(): void | Promise { + override register(): void { this.navNodeManagerService.onCanOpen.addHandler(this.canOpenHandler.bind(this)); this.navNodeManagerService.navigator.addHandler(this.navigationHandler.bind(this)); this.actionService.addHandler({ id: 'scripts-base-handler', - isActionApplicable: (context, action): boolean => { - const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); - - if (!state) { - return false; - } + actions: [ACTION_SAVE_AS_SCRIPT], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], + isActionApplicable: (context): boolean => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; if (!this.projectsService.activeProjects.some(project => project.canEditResources)) { return false; @@ -79,142 +101,15 @@ export class PluginBootstrap extends Bootstrap { const dataSource = this.sqlDataSourceService.get(state.editorId); - if (action === ACTION_SAVE_AS_SCRIPT) { - return dataSource instanceof MemorySqlDataSource || dataSource instanceof LocalStorageSqlDataSource; - } - - if (action === ACTION_SAVE) { - return dataSource?.isAutoSaveEnabled === false; - } - - return false; + return dataSource instanceof MemorySqlDataSource || dataSource instanceof LocalStorageSqlDataSource; }, - handler: async (context, action) => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); - - let dataSource: ISqlDataSource | ResourceSqlDataSource | undefined = this.sqlDataSourceService.get(state.editorId); - - if (!dataSource) { - return; - } - + handler: (context, action) => { if (action === ACTION_SAVE_AS_SCRIPT) { - let projectId = dataSource.executionContext?.projectId ?? null; - await this.projectInfoResource.load(CachedMapAllKey); - const name = getSqlEditorName(state, dataSource); - - if (projectId) { - const project = this.projectInfoResource.get(projectId); - - if (!project?.canEditResources) { - projectId = null; - } - } - - const result = await this.commonDialogService.open(SaveScriptDialog, { - defaultScriptName: name, - projectId, - validation: async ({ name, projectId }, setMessage) => { - const trimmedName = name.trim(); - - if (!projectId || !trimmedName.length) { - return false; - } - - if (!RESOURCE_NAME_REGEX.test(name)) { - setMessage('plugin_resource_manager_scripts_script_name_invalid_characters_message'); - return false; - } - - const project = this.projectInfoResource.get(projectId); - const nameWithExtension = this.projectInfoResource.getNameWithExtension(projectId, SCRIPTS_TYPE_ID, trimmedName); - const rootFolder = project ? this.resourceManagerScriptsService.getRootFolder(project) : undefined; - const key = getRmResourcePath(projectId, rootFolder); - - try { - await this.resourceManagerResource.load(CachedTreeChildrenKey(key)); - return !this.resourceManagerResource.has(createPath(key, nameWithExtension)); - } catch (exception: any) { - return false; - } - }, - }); - - if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) { - try { - projectId = result.projectId; - - if (!projectId) { - throw new Error('Project not selected'); - } - - const project = this.projectInfoResource.get(projectId); - if (!project) { - throw new Error('Project not found'); - } - - const nameWithoutExtension = result.name.trim(); - const scriptName = this.projectInfoResource.getNameWithExtension(projectId, SCRIPTS_TYPE_ID, nameWithoutExtension); - const scriptsRootFolder = this.resourceManagerScriptsService.getRootFolder(project); - const folderResourceKey = getResourceKeyFromNodeId(createPath(RESOURCES_NODE_PATH, projectId, scriptsRootFolder)); - - if (!folderResourceKey) { - this.notificationService.logError({ title: 'ui_error', message: 'plugin_sql_editor_navigation_tab_resource_save_script_error' }); - return; - } - - const resourceKey = createPath(folderResourceKey, scriptName); - - await this.resourceManagerScriptsService.createScript(resourceKey, dataSource.executionContext, dataSource.script); - - dataSource = this.sqlDataSourceService.create(state, ResourceSqlDataSource.key, { - script: dataSource.script, - executionContext: dataSource.executionContext, - }); - - (dataSource as ResourceSqlDataSource).setResourceKey(resourceKey); - - this.notificationService.logSuccess({ - title: 'plugin_sql_editor_navigation_tab_resource_save_script_success', - message: nameWithoutExtension, - }); - - if (!this.resourceManagerScriptsService.active) { - this.resourceManagerScriptsService.togglePanel(); - } - } catch (exception) { - this.notificationService.logException(exception as any, 'plugin_sql_editor_navigation_tab_resource_save_script_error'); - } - } - } - - if (action === ACTION_SAVE) { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); - const source = this.sqlDataSourceService.get(state.editorId) as ResourceSqlDataSource | undefined; - - if (!source) { - return; - } - - await source.save(); + this.saveAsScriptHandler(context); } }, - isDisabled: (context, action) => { - if (action === ACTION_SAVE) { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); - const source = this.sqlDataSourceService.get(state.editorId) as ResourceSqlDataSource | undefined; - - if (!source) { - return true; - } - - return source.isLoading() || source.isSaved; - } - - return false; - }, getActionInfo: (context, action) => { - if (action === ACTION_SAVE_AS_SCRIPT || action === ACTION_SAVE) { + if (action === ACTION_SAVE_AS_SCRIPT) { return { ...action.info, label: '', @@ -225,43 +120,128 @@ export class PluginBootstrap extends Bootstrap { }, }); + this.sqlEditorView.registerAction(ACTION_SAVE_AS_SCRIPT); + this.menuService.addCreator({ + menus: [SQL_EDITOR_TOOLS_MENU], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], isApplicable: context => { - const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); - - if (!state) { - return false; - } + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(state.editorId); - return ( - this.resourceManagerService.enabled && - context.get(DATA_CONTEXT_MENU) === SQL_EDITOR_TOOLS_MENU && - !!dataSource?.hasFeature(ESqlDataSourceFeatures.script) - ); + return this.resourceManagerService.enabled && !!dataSource?.hasFeature(ESqlDataSourceFeatures.script); }, - getItems: (context, items) => [...items, ACTION_SAVE_AS_SCRIPT, ACTION_SAVE], + getItems: (context, items) => [ACTION_SAVE_AS_SCRIPT, ...items], }); this.keyBindingService.addKeyBindingHandler({ - id: 'script-save', - binding: KEY_BINDING_SAVE, - isBindingApplicable: (context, action) => action === ACTION_SAVE, - handler: async context => { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); - const source = this.sqlDataSourceService.get(state.editorId) as ResourceSqlDataSource | undefined; - - if (!source) { - return; + id: 'save-as-script', + binding: KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT, + actions: [ACTION_SAVE_AS_SCRIPT], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], + isBindingApplicable: (_, action) => action === ACTION_SAVE_AS_SCRIPT, + handler: this.saveAsScriptHandler.bind(this), + }); + } + + private async saveAsScriptHandler(context: IDataContextProvider) { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + + let dataSource: ISqlDataSource | ResourceSqlDataSource | undefined = this.sqlDataSourceService.get(state.editorId); + + if (!dataSource) { + return; + } + + let projectId = dataSource.executionContext?.projectId ?? null; + await this.projectInfoResource.load(CachedMapAllKey); + const name = getSqlEditorName(state, dataSource); + + if (projectId) { + const project = this.projectInfoResource.get(projectId); + + if (!project?.canEditResources) { + projectId = null; + } + } + + const result = await this.commonDialogService.open(SaveScriptDialog, { + defaultScriptName: name, + projectId, + validation: async ({ name, projectId }, setMessage) => { + const trimmedName = name.trim(); + + if (!projectId || !trimmedName.length) { + return false; } - await source.save(); + if (!RESOURCE_NAME_REGEX.test(trimmedName)) { + setMessage('plugin_resource_manager_scripts_script_name_invalid_characters_message'); + return false; + } + + const project = this.projectInfoResource.get(projectId); + const nameWithExtension = this.projectInfoResource.getNameWithExtension(projectId, SCRIPTS_TYPE_ID, trimmedName); + const rootFolder = project ? this.resourceManagerScriptsService.getRootFolder(project) : undefined; + const key = getRmResourcePath(projectId, rootFolder); + + try { + await this.resourceManagerResource.load(CachedTreeChildrenKey(key)); + return !this.resourceManagerResource.has(createPath(key, nameWithExtension)); + } catch (exception: any) { + return false; + } }, }); - } - load(): void | Promise {} + if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) { + try { + projectId = result.projectId; + + if (!projectId) { + throw new Error('Project not selected'); + } + + const project = this.projectInfoResource.get(projectId); + if (!project) { + throw new Error('Project not found'); + } + + const nameWithoutExtension = result.name.trim(); + const scriptName = this.projectInfoResource.getNameWithExtension(projectId, SCRIPTS_TYPE_ID, nameWithoutExtension); + const scriptsRootFolder = this.resourceManagerScriptsService.getRootFolder(project); + const folderResourceKey = getResourceKeyFromNodeId(createPath(RESOURCES_NODE_PATH, projectId, scriptsRootFolder)); + + if (!folderResourceKey) { + this.notificationService.logError({ title: 'ui_error', message: 'plugin_sql_editor_navigation_tab_resource_save_script_error' }); + return; + } + + const resourceKey = createPath(folderResourceKey, scriptName); + + await this.resourceManagerScriptsService.createScript(resourceKey, dataSource.executionContext, dataSource.script); + + dataSource = this.sqlDataSourceService.create(state, ResourceSqlDataSource.key, { + script: dataSource.script, + executionContext: dataSource.executionContext, + }); + + (dataSource as ResourceSqlDataSource).setResourceKey(resourceKey); + + this.notificationService.logSuccess({ + title: 'plugin_sql_editor_navigation_tab_resource_save_script_success', + message: nameWithoutExtension, + }); + + if (!this.resourceManagerScriptsService.active) { + this.resourceManagerScriptsService.togglePanel(); + } + } catch (exception) { + this.notificationService.logException(exception as any, 'plugin_sql_editor_navigation_tab_resource_save_script_error'); + } + } + } private canOpenHandler(data: INodeNavigationData, contexts: IExecutionContextProvider): void { const nodeInfo = contexts.getContext(this.navNodeManagerService.navigationNavNodeContext); @@ -285,9 +265,7 @@ export class PluginBootstrap extends Bootstrap { const resource = await this.resourceManagerResource.load(resourceKey); - const maxSize = this.sqlEditorSettingsService.settings.isValueDefault('maxFileSize') - ? this.sqlEditorSettingsService.deprecatedSettings.getValue('maxFileSize') - : this.sqlEditorSettingsService.settings.getValue('maxFileSize'); + const maxSize = this.sqlEditorSettingsService.maxFileSize; const size = Math.round(resource.length / 1000); // kilobyte if (size > maxSize) { diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceEditorSettingsService.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceEditorSettingsService.ts index 92a120267e..b05c4eb7d8 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceEditorSettingsService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceEditorSettingsService.ts @@ -1,22 +1,34 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; +import { SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema } from '@cloudbeaver/core-utils'; -const defaultSettings = {}; +const defaultSettings = schema.object({ + 'plugin.sql-editor-navigation-tab-resource': schema.object({}), +}); export type ResourceEditorSettings = typeof defaultSettings; -@injectable() +@injectable(() => [SettingsProviderService, SettingsManagerService]) export class ResourceEditorSettingsService { - readonly settings: PluginSettings; + readonly settings: SettingsProvider; - constructor(private readonly pluginManagerService: PluginManagerService) { - this.settings = this.pluginManagerService.createSettings('sql-editor-navigation-tab-resource', 'plugin', defaultSettings); + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { + this.settings = this.settingsProviderService.createSettings(defaultSettings); + + this.registerSettings(); + } + + private registerSettings() { + this.settingsManagerService.registerSettings(() => []); } } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts index a724107bb6..c58d2dc401 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,22 +10,20 @@ import { action, computed, makeObservable, observable, runInAction, toJS } from import { ConnectionInfoResource, createConnectionParam, - IConnectionExecutionContextInfo, + type IConnectionExecutionContextInfo, NOT_INITIALIZED_CONTEXT_ID, } from '@cloudbeaver/core-connections'; import { TaskScheduler } from '@cloudbeaver/core-executor'; -import type { ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { isResourceAlias, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import type { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; +import { isResourceAlias, type ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import { getRmResourceKey, ResourceManagerResource } from '@cloudbeaver/core-resource-manager'; -import { debounce, getPathName, isArraysEqual, isNotNullDefined, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils'; +import type { NetworkStateService } from '@cloudbeaver/core-root'; +import { debounce, getPathName, isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils'; +import { isNotNullDefined } from '@dbeaver/js-helpers'; import { SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts'; -import { BaseSqlDataSource, ESqlDataSourceFeatures, SqlEditorService } from '@cloudbeaver/plugin-sql-editor'; +import { BaseSqlDataSource, ESqlDataSourceFeatures, SqlEditorService, type ISqlEditorCursor } from '@cloudbeaver/plugin-sql-editor'; -import type { IResourceSqlDataSourceState } from './IResourceSqlDataSourceState'; - -interface IResourceInfo { - isReadonly?: (dataSource: ResourceSqlDataSource) => boolean; -} +import type { IResourceSqlDataSourceState } from './IResourceSqlDataSourceState.js'; interface IResourceActions { rename(dataSource: ResourceSqlDataSource, key: string, name: string): Promise; @@ -40,9 +38,10 @@ interface IResourceActions { } const VALUE_SYNC_DELAY = 1 * 1000; +const MESSAGE_DISPLAY_DELAY = 4 * 1000; export class ResourceSqlDataSource extends BaseSqlDataSource { - static key = 'resource'; + static override key = 'resource'; get name(): string | null { if (!this.resourceKey || !this.projectId) { @@ -66,7 +65,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { return this.state.baseExecutionContext; } - get projectId(): string | null { + override get projectId(): string | null { if (this.resourceKey === undefined) { return super.projectId; } @@ -90,7 +89,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { return this.lastAction; } - get features(): ESqlDataSourceFeatures[] { + override get features(): ESqlDataSourceFeatures[] { if (this.isReadonly()) { return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable]; } @@ -98,12 +97,11 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable, ESqlDataSourceFeatures.setName]; } - get isAutoSaveEnabled(): boolean { + override get isAutoSaveEnabled(): boolean { return this.sqlEditorService.autoSave; } private actions?: IResourceActions; - private info?: IResourceInfo; private lastAction: (() => Promise) | undefined; private state!: IResourceSqlDataSourceState; @@ -112,10 +110,12 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { private resourceUseKeyId: string | null; constructor( + private readonly networkStateService: NetworkStateService, private readonly projectInfoResource: ProjectInfoResource, private readonly connectionInfoResource: ConnectionInfoResource, private readonly resourceManagerResource: ResourceManagerResource, private readonly sqlEditorService: SqlEditorService, + private readonly projectsService: ProjectsService, state: IResourceSqlDataSourceState, ) { super(); @@ -143,19 +143,31 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { }); } - isReadonly(): boolean { - return !this.isLoaded() || this.info?.isReadonly?.(this) === true; + override isReadonly(): boolean { + if (!this.projectId || !this.networkStateService.state) { + return true; + } + + const project = this.projectInfoResource.get(this.projectId); + + return !this.isLoaded() || !project?.canEditResources; } - isOutdated(): boolean { + override isOutdated(): boolean { + if (this.projectId) { + if (this.projectInfoResource.isOutdated(this.projectId)) { + return true; + } + } + return this.resourceKey !== undefined && super.isOutdated(); } - isLoaded(): boolean { + override isLoaded(): boolean { return this.resourceKey === undefined || super.isLoaded() || this.loaded; } - isLoading(): boolean { + override isLoading(): boolean { return this.scheduler.executing; } @@ -189,11 +201,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { this.actions = actions; } - setInfo(info?: IResourceInfo): void { - this.info = info; - } - - setName(name: string | null): void { + override setName(name: string | null): void { name = name?.trim() ?? null; if (!name || name === this.name) { return; @@ -207,35 +215,29 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { }); } - setProject(projectId: string | null): void { + override setProject(projectId: string | null): void { super.setProject(projectId); } - setScript(script: string): void { + override setScript(script: string, source?: string, cursor?: ISqlEditorCursor): void { const previous = this.state.script; if (previous === script) { return; } this.state.script = script; - super.setScript(script); + super.setScript(script, source, cursor); if (this.isAutoSaveEnabled) { this.debouncedWrite(); } } - setExecutionContext(executionContext: IConnectionExecutionContextInfo | undefined): void { + override setExecutionContext(executionContext: IConnectionExecutionContextInfo | undefined): void { if (executionContext) { executionContext = JSON.parse(JSON.stringify(toJS(executionContext) ?? {})); } - const projectId = executionContext?.projectId; - - if (this.resourceKey && isNotNullDefined(projectId) && getRmResourceKey(this.resourceKey).projectId !== projectId) { - throw new Error('Resource SQL Data Source and Execution context projects don\t match'); - } - if (!isObjectsEqual(toJS(this.state.executionContext), executionContext)) { const initNew = !isValuesEqual(executionContext?.connectionId, this.executionContext?.connectionId, undefined) || @@ -263,14 +265,19 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { } } - async load(): Promise { + override async load(): Promise { if (this.state.resourceKey && !this.resourceUseKeyId) { this.resourceUseKeyId = this.resourceManagerResource.useTracker.use(this.state.resourceKey); } + + if (this.projectId) { + await this.projectInfoResource.load(this.projectId); + } + await this.read(); } - async save(): Promise { + override async save(): Promise { try { await this.write(); await this.saveProperties(); @@ -280,7 +287,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { } } - dispose(): void { + override dispose(): void { super.dispose(); this.resourceManagerResource.onItemUpdate.removeHandler(this.syncResource); if (this.state.resourceKey && this.resourceUseKeyId) { @@ -361,12 +368,28 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { try { this.exception = null; + const projectId = this.executionContext?.projectId; + const resourceProjectId = getRmResourceKey(this.resourceKey).projectId; + const userProjectId = this.projectsService.userProject?.id; + + if (isNotNullDefined(projectId) && resourceProjectId !== projectId && resourceProjectId !== userProjectId) { + this.message = 'plugin_sql_editor_navigation_tab_script_state_different_project'; + + // TODO: this leads to long delay before saving, fast logout/login will lead to broken state + await new Promise(resolve => setTimeout(resolve, MESSAGE_DISPLAY_DELAY)); + return; + } + if (!this.isReadonly()) { this.message = 'plugin_sql_editor_navigation_tab_script_state_updating'; const executionContext = await this.actions.setProperties(this, this.resourceKey, this.executionContext); this.setExecutionContext(executionContext); this.setBaseExecutionContext(this.executionContext); + } else { + this.message = 'plugin_sql_editor_navigation_tab_script_state_readonly'; + // TODO: this leads to long delay before saving, fast logout/login will lead to broken state + await new Promise(resolve => setTimeout(resolve, MESSAGE_DISPLAY_DELAY)); } } finally { this.message = undefined; @@ -388,8 +411,9 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { if (!this.isIncomingChanges) { this.message = 'plugin_sql_editor_navigation_tab_script_state_saving'; - await this.actions.write(this, this.resourceKey, this.script); - this.setBaseScript(this.script); + const script = this.script; + await this.actions.write(this, this.resourceKey, script); + this.setBaseScript(script); } } finally { this.message = undefined; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts index fb006be0b6..f99629907c 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts @@ -1,23 +1,23 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, makeObservable, observable, untracked } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoResource, IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; +import { ConnectionInfoResource, type IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; -import { getRmResourceKey, IResourceManagerMoveData, ResourceManagerResource } from '@cloudbeaver/core-resource-manager'; -import { NetworkStateService, WindowEventsService } from '@cloudbeaver/core-root'; -import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; -import { createPath, getPathParent, throttle } from '@cloudbeaver/core-utils'; +import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; +import { type ICachedTreeMoveData, resourceKeyList, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { ResourceManagerResource } from '@cloudbeaver/core-resource-manager'; +import { NetworkStateService } from '@cloudbeaver/core-root'; +import { StorageService } from '@cloudbeaver/core-storage'; +import { createPath, getPathParent } from '@cloudbeaver/core-utils'; import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; import { NavResourceNodeService } from '@cloudbeaver/plugin-navigation-tree-rm'; import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager'; @@ -25,14 +25,30 @@ import { ResourceManagerScriptsService, SCRIPTS_TYPE_ID } from '@cloudbeaver/plu import { createSqlDataSourceHistoryInitialState, getSqlEditorName, SqlDataSourceService, SqlEditorService } from '@cloudbeaver/plugin-sql-editor'; import { SqlEditorTabService } from '@cloudbeaver/plugin-sql-editor-navigation-tab'; -import type { IResourceSqlDataSourceState } from './IResourceSqlDataSourceState'; -import { ResourceSqlDataSource } from './ResourceSqlDataSource'; -import { SqlEditorTabResourceService } from './SqlEditorTabResourceService'; +import type { IResourceSqlDataSourceState } from './IResourceSqlDataSourceState.js'; +import { ResourceSqlDataSource } from './ResourceSqlDataSource.js'; +import { SqlEditorTabResourceService } from './SqlEditorTabResourceService.js'; const RESOURCE_TAB_STATE = 'sql_editor_resource_tab_state'; -const SYNC_DELAY = 5 * 60 * 1000; -@injectable() +@injectable(() => [ + ConnectionInfoResource, + NetworkStateService, + SqlDataSourceService, + CommonDialogService, + NavResourceNodeService, + NotificationService, + ResourceManagerService, + SqlEditorTabService, + NavigationTabsService, + ResourceManagerResource, + ResourceManagerScriptsService, + ProjectInfoResource, + SqlEditorTabResourceService, + SqlEditorService, + ProjectsService, + StorageService, +]) export class ResourceSqlDataSourceBootstrap extends Bootstrap { private readonly dataSourceStateState = new Map(); @@ -49,21 +65,20 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { private readonly resourceManagerResource: ResourceManagerResource, private readonly resourceManagerScriptsService: ResourceManagerScriptsService, private readonly projectInfoResource: ProjectInfoResource, - private readonly windowEventsService: WindowEventsService, private readonly sqlEditorTabResourceService: SqlEditorTabResourceService, private readonly sqlEditorService: SqlEditorService, - localStorageSaveService: LocalStorageSaveService, + private readonly projectsService: ProjectsService, + storageService: StorageService, ) { super(); this.dataSourceStateState = new Map(); - this.focusChangeHandler = throttle(this.focusChangeHandler.bind(this), SYNC_DELAY, false); makeObservable(this, { createState: action, dataSourceStateState: observable, }); - localStorageSaveService.withAutoSave( + storageService.registerSettings( RESOURCE_TAB_STATE, this.dataSourceStateState, () => new Map(), @@ -86,8 +101,7 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { ); } - register(): void | Promise { - this.windowEventsService.onFocusChange.addHandler(this.focusChangeHandler.bind(this)); + override register(): void | Promise { this.resourceManagerResource.onItemDelete.addHandler(this.resourceDeleteHandler.bind(this)); this.resourceManagerResource.onMove.addHandler(this.resourceMoveHandler.bind(this)); @@ -95,10 +109,12 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { key: ResourceSqlDataSource.key, getDataSource: (editorId, options) => { const dataSource = new ResourceSqlDataSource( + this.networkStateService, this.projectInfoResource, this.connectionInfoResource, this.resourceManagerResource, this.sqlEditorService, + this.projectsService, this.createState(editorId, options?.script), ); @@ -118,20 +134,6 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { setProperties: this.setProperties.bind(this), }); - dataSource.setInfo({ - isReadonly: (dataSource: ResourceSqlDataSource) => { - if (!dataSource.resourceKey) { - return true; - } - const resourceKey = getRmResourceKey(dataSource.resourceKey); - - untracked(() => this.projectInfoResource.load(CachedMapAllKey)); - const project = this.projectInfoResource.get(resourceKey.projectId); - - return !this.networkStateService.state || !project?.canEditResources; - }, - }); - return dataSource; }, onDestroy: (_, editorId) => this.deleteState(editorId), @@ -165,8 +167,6 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { }); } - load(): void | Promise {} - private createState(editorId: string, script?: string, resourceKey?: string): IResourceSqlDataSourceState { let state = this.dataSourceStateState.get(editorId); @@ -188,23 +188,7 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { this.dataSourceStateState.delete(editorId); } - private async focusChangeHandler(focused: boolean) { - if (!this.resourceManagerService.enabled) { - return; - } - - if (focused) { - const dataSources = this.sqlDataSourceService.dataSources - .filter(([, dataSource]) => dataSource instanceof ResourceSqlDataSource) - .map(([, dataSource]) => dataSource as ResourceSqlDataSource); - - for (const dataSource of dataSources) { - dataSource.markOutdated(); - } - } - } - - private resourceMoveHandler(data: IResourceManagerMoveData) { + private resourceMoveHandler(data: ICachedTreeMoveData) { if (!this.resourceManagerService.enabled) { return; } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/SqlEditorTabResourceService.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/SqlEditorTabResourceService.ts index 5141e70d96..6859f0d119 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/SqlEditorTabResourceService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/SqlEditorTabResourceService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,11 +9,14 @@ import { injectable } from '@cloudbeaver/core-di'; import { SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor'; import { SqlEditorTabService } from '@cloudbeaver/plugin-sql-editor-navigation-tab'; -import { ResourceSqlDataSource } from './ResourceSqlDataSource'; +import { ResourceSqlDataSource } from './ResourceSqlDataSource.js'; -@injectable() +@injectable(() => [SqlEditorTabService, SqlDataSourceService]) export class SqlEditorTabResourceService { - constructor(private readonly sqlEditorTabService: SqlEditorTabService, private readonly sqlDataSourceService: SqlDataSourceService) {} + constructor( + private readonly sqlEditorTabService: SqlEditorTabService, + private readonly sqlDataSourceService: SqlDataSourceService, + ) {} getResourceTab(key: string) { const dataSource = this.sqlDataSourceService.dataSources.find( diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/index.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/index.ts index e2e38fb300..44c440ab03 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/index.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/index.ts @@ -1,3 +1,14 @@ -import { manifest } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { manifest } from './manifest.js'; + +export * from './KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT.js'; export default manifest; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/en.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/en.ts index d32911cf65..f324c2c930 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/en.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/en.ts @@ -4,6 +4,11 @@ export default [ ['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'], ['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'], ['plugin_sql_editor_navigation_tab_script_state_updating', 'Updating script info...'], + [ + 'plugin_sql_editor_navigation_tab_script_state_different_project', + 'The connection project differs from the script project—the connection change is not saved.', + ], + ['plugin_sql_editor_navigation_tab_script_state_readonly', 'This is readonly script. Any changes will not be saved.'], ['plugin_sql_editor_navigation_tab_resource_save_script_success', 'Script successfully saved'], ['plugin_sql_editor_navigation_tab_resource_open_script_error', 'Failed to open the script'], ['plugin_sql_editor_navigation_tab_resource_save_script_error', 'Error occurred while trying to save the script'], diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/fr.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/fr.ts new file mode 100644 index 0000000000..8fe06ed6a7 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/fr.ts @@ -0,0 +1,22 @@ +export default [ + ['plugin_sql_editor_navigation_tab_resource_save_title', 'Enregistrer comme script'], + ['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renommer le script...'], + ['plugin_sql_editor_navigation_tab_script_state_reading', 'Lecture du script...'], + ['plugin_sql_editor_navigation_tab_script_state_saving', 'Enregistrement du script...'], + ['plugin_sql_editor_navigation_tab_script_state_updating', 'Mise à jour des informations du script...'], + [ + 'plugin_sql_editor_navigation_tab_script_state_different_project', + "Le projet de connexion diffère du projet du script, le changement de connexion n'est pas enregistré.", + ], + ['plugin_sql_editor_navigation_tab_script_state_readonly', 'This is readonly script. Any changes will not be saved.'], + ['plugin_sql_editor_navigation_tab_resource_save_script_success', 'Script enregistré avec succès'], + ['plugin_sql_editor_navigation_tab_resource_open_script_error', "Échec de l'ouverture du script"], + ['plugin_sql_editor_navigation_tab_resource_save_script_error', "Erreur lors de la tentative d'enregistrement du script"], + ['plugin_sql_editor_navigation_tab_resource_update_script_error', 'Échec de la mise à jour du script'], + ['plugin_sql_editor_navigation_tab_resource_sync_script_error', "Échec de la synchronisation de la requête de l'éditeur"], + ['plugin_sql_editor_navigation_tab_resource_save_script_error_confirmation_title', "Impossible d'enregistrer le script"], + [ + 'plugin_sql_editor_navigation_tab_resource_save_script_error_confirmation_message', + "Une erreur est survenue lors de la tentative d'enregistrement du script. Fermer l'onglet quand même ?", + ], +]; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/it.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/it.ts index d32911cf65..f324c2c930 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/it.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/it.ts @@ -4,6 +4,11 @@ export default [ ['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'], ['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'], ['plugin_sql_editor_navigation_tab_script_state_updating', 'Updating script info...'], + [ + 'plugin_sql_editor_navigation_tab_script_state_different_project', + 'The connection project differs from the script project—the connection change is not saved.', + ], + ['plugin_sql_editor_navigation_tab_script_state_readonly', 'This is readonly script. Any changes will not be saved.'], ['plugin_sql_editor_navigation_tab_resource_save_script_success', 'Script successfully saved'], ['plugin_sql_editor_navigation_tab_resource_open_script_error', 'Failed to open the script'], ['plugin_sql_editor_navigation_tab_resource_save_script_error', 'Error occurred while trying to save the script'], diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/ru.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/ru.ts index 8e20e5cff1..5527769668 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/ru.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/ru.ts @@ -4,6 +4,11 @@ export default [ ['plugin_sql_editor_navigation_tab_script_state_reading', 'Чтение скрипта...'], ['plugin_sql_editor_navigation_tab_script_state_saving', 'Сохранение скрипта...'], ['plugin_sql_editor_navigation_tab_script_state_updating', 'Обновление информации о скрипте...'], + [ + 'plugin_sql_editor_navigation_tab_script_state_different_project', + 'Проект подключения отличается от проекта скрипта. Изменение подключения не сохранено.', + ], + ['plugin_sql_editor_navigation_tab_script_state_readonly', 'Этот скрипт только для чтения. Изменения не будут сохранены.'], ['plugin_sql_editor_navigation_tab_resource_save_script_success', 'Скрипт успешно сохранен'], ['plugin_sql_editor_navigation_tab_resource_open_script_error', 'Не удалось открыть скрипт'], ['plugin_sql_editor_navigation_tab_resource_save_script_error', 'Возникла ошибка при попытки сохранить скрипт'], diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/vi.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/vi.ts new file mode 100644 index 0000000000..51ee8e4c1e --- /dev/null +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/vi.ts @@ -0,0 +1,16 @@ +export default [ + ['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Lưu dưới dạng script'], + ['plugin_sql_editor_navigation_tab_script_state_renaming', 'Đang đổi tên script...'], + ['plugin_sql_editor_navigation_tab_script_state_reading', 'Đang đọc script...'], + ['plugin_sql_editor_navigation_tab_script_state_saving', 'Đang lưu script...'], + ['plugin_sql_editor_navigation_tab_script_state_updating', 'Đang cập nhật thông tin script...'], + ['plugin_sql_editor_navigation_tab_script_state_different_project', 'Dự án kết nối khác với dự án script—thay đổi kết nối không được lưu.'], + ['plugin_sql_editor_navigation_tab_script_state_readonly', 'Đây là script chỉ đọc. Mọi thay đổi sẽ không được lưu.'], + ['plugin_sql_editor_navigation_tab_resource_save_script_success', 'Script đã được lưu thành công'], + ['plugin_sql_editor_navigation_tab_resource_open_script_error', 'Không thể mở script'], + ['plugin_sql_editor_navigation_tab_resource_save_script_error', 'Đã xảy ra lỗi khi cố gắng lưu script'], + ['plugin_sql_editor_navigation_tab_resource_update_script_error', 'Không thể cập nhật script'], + ['plugin_sql_editor_navigation_tab_resource_sync_script_error', 'Không thể đồng bộ truy vấn của trình soạn thảo'], + ['plugin_sql_editor_navigation_tab_resource_save_script_error_confirmation_title', 'Không thể lưu script'], + ['plugin_sql_editor_navigation_tab_resource_save_script_error_confirmation_message', 'Đã xảy ra lỗi khi cố gắng lưu script. Đóng tab?'], +]; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/zh.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/zh.ts index 39c633324c..50e1d6720f 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/zh.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/zh.ts @@ -1,17 +1,16 @@ export default [ - ['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script'], - ['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renaming script...'], - ['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'], - ['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'], - ['plugin_sql_editor_navigation_tab_script_state_updating', 'Updating script info...'], + ['plugin_sql_editor_navigation_tab_resource_save_script_title', '保存为脚本'], + ['plugin_sql_editor_navigation_tab_script_state_renaming', '重命名脚本中...'], + ['plugin_sql_editor_navigation_tab_script_state_reading', '读取脚本中...'], + ['plugin_sql_editor_navigation_tab_script_state_saving', '保存脚本中...'], + ['plugin_sql_editor_navigation_tab_script_state_updating', '更新脚本信息...'], + ['plugin_sql_editor_navigation_tab_script_state_different_project', '连接项目与脚本项目不同 - 不会保存连接更改。'], + ['plugin_sql_editor_navigation_tab_script_state_readonly', 'This is readonly script. Any changes will not be saved.'], ['plugin_sql_editor_navigation_tab_resource_save_script_success', '脚本保存成功'], ['plugin_sql_editor_navigation_tab_resource_open_script_error', '打开脚本失败'], ['plugin_sql_editor_navigation_tab_resource_save_script_error', '打开脚本出错'], ['plugin_sql_editor_navigation_tab_resource_update_script_error', '更新脚本出错'], ['plugin_sql_editor_navigation_tab_resource_sync_script_error', '同步查询失败'], - ['plugin_sql_editor_navigation_tab_resource_save_script_error_confirmation_title', 'Unable to save the script'], - [ - 'plugin_sql_editor_navigation_tab_resource_save_script_error_confirmation_message', - 'Error occurred while trying to save the script. Close the tab anyway?', - ], + ['plugin_sql_editor_navigation_tab_resource_save_script_error_confirmation_title', '无法保存脚本'], + ['plugin_sql_editor_navigation_tab_resource_save_script_error_confirmation_message', '尝试保存脚本时发生错误,是否关闭标签页?'], ]; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/manifest.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/manifest.ts index 804bf79b76..08eb52e6f1 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/manifest.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/manifest.ts @@ -1,18 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { LocaleService } from './LocaleService'; -import { PluginBootstrap } from './PluginBootstrap'; -import { ResourceSqlDataSourceBootstrap } from './ResourceSqlDataSourceBootstrap'; -import { SqlEditorTabResourceService } from './SqlEditorTabResourceService'; - export const manifest: PluginManifest = { info: { name: 'Sql Editor Script plugin' }, - providers: [PluginBootstrap, LocaleService, SqlEditorTabResourceService, ResourceSqlDataSourceBootstrap], }; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/module.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/module.ts new file mode 100644 index 0000000000..ba0d39903b --- /dev/null +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/module.ts @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { ResourceSqlDataSourceBootstrap } from './ResourceSqlDataSourceBootstrap.js'; +import { SqlEditorTabResourceService } from './SqlEditorTabResourceService.js'; +import { PluginBootstrap } from './PluginBootstrap.js'; +import { LocaleService } from './LocaleService.js'; +import { ResourceEditorSettingsService } from './ResourceEditorSettingsService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-sql-editor-navigation-tab-script', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Dependency, proxy(ResourceEditorSettingsService)) + .addSingleton(Bootstrap, ResourceSqlDataSourceBootstrap) + .addSingleton(Bootstrap, PluginBootstrap) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(ResourceEditorSettingsService) + .addSingleton(SqlEditorTabResourceService); + }, +}); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/tsconfig.json b/webapp/packages/plugin-sql-editor-navigation-tab-script/tsconfig.json index 04a6b93151..d7eb7c7753 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/tsconfig.json +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/tsconfig.json @@ -1,76 +1,86 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-navigation-tabs/tsconfig.json" + "path": "../../common-typescript/@dbeaver/js-helpers" }, { - "path": "../plugin-navigation-tree-rm/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../plugin-resource-manager/tsconfig.json" + "path": "../core-cli" }, { - "path": "../plugin-resource-manager-scripts/tsconfig.json" + "path": "../core-connections" }, { - "path": "../plugin-sql-editor/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../plugin-sql-editor-navigation-tab/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-executor" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-navigation-tree" }, { - "path": "../core-executor/tsconfig.json" + "path": "../core-projects" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-navigation-tree/tsconfig.json" + "path": "../core-resource-manager" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-projects/tsconfig.json" + "path": "../core-settings" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-storage" }, { - "path": "../core-resource-manager/tsconfig.json" + "path": "../core-utils" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-view" }, { - "path": "../core-settings/tsconfig.json" + "path": "../plugin-navigation-tabs" }, { - "path": "../core-utils/tsconfig.json" + "path": "../plugin-navigation-tree-rm" }, { - "path": "../core-view/tsconfig.json" + "path": "../plugin-resource-manager" + }, + { + "path": "../plugin-resource-manager-scripts" + }, + { + "path": "../plugin-sql-editor" + }, + { + "path": "../plugin-sql-editor-navigation-tab" } ], "include": [ @@ -82,7 +92,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/package.json b/webapp/packages/plugin-sql-editor-navigation-tab/package.json index 485bef6d67..3135da7557 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/package.json +++ b/webapp/packages/plugin-sql-editor-navigation-tab/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-sql-editor-navigation-tab", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,42 +11,48 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-connections": "~0.1.0", - "@cloudbeaver/plugin-navigation-tabs": "~0.1.0", - "@cloudbeaver/plugin-sql-editor": "~0.1.0", - "@cloudbeaver/plugin-top-app-bar": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-data-context": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-extensions": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-projects": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-extensions": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-navigation-tree": "workspace:*", + "@cloudbeaver/core-projects": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-connections": "workspace:*", + "@cloudbeaver/plugin-navigation-tabs": "workspace:*", + "@cloudbeaver/plugin-sql-editor": "workspace:*", + "@cloudbeaver/plugin-top-app-bar": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/ACTION_SQL_EDITOR_NEW.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/ACTION_SQL_EDITOR_NEW.ts index 2775a9ef9f..97998902cb 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/ACTION_SQL_EDITOR_NEW.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/ACTION_SQL_EDITOR_NEW.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/ACTION_SQL_EDITOR_OPEN.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/ACTION_SQL_EDITOR_OPEN.ts index 0bc362ce3d..ff62b82e7d 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/ACTION_SQL_EDITOR_OPEN.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/ACTION_SQL_EDITOR_OPEN.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/DATA_CONTEXT_SQL_EDITOR_TAB.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/DATA_CONTEXT_SQL_EDITOR_TAB.ts index f80201212b..6f08e3d2ab 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/DATA_CONTEXT_SQL_EDITOR_TAB.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/DATA_CONTEXT_SQL_EDITOR_TAB.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/LocaleService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/LocaleService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SESSION_ACTION_OPEN_SQL_EDITOR.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SESSION_ACTION_OPEN_SQL_EDITOR.ts index dc46cd10d0..a708182b51 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SESSION_ACTION_OPEN_SQL_EDITOR.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SESSION_ACTION_OPEN_SQL_EDITOR.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SQL_EDITOR_SOURCE_ACTION.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SQL_EDITOR_SOURCE_ACTION.ts index 8ad2e4c430..f2df4eeb11 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SQL_EDITOR_SOURCE_ACTION.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SQL_EDITOR_SOURCE_ACTION.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts index 7281342f2e..e16abc05f0 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts @@ -1,17 +1,17 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { RenameDialog } from '@cloudbeaver/core-blocks'; +import { importLazyComponent, RenameDialog } from '@cloudbeaver/core-blocks'; import { - Connection, + type Connection, ConnectionInfoResource, createConnectionParam, DATA_CONTEXT_CONNECTION, - IConnectionInfoParams, + type IConnectionInfoParams, isConnectionProvider, isObjectCatalogProvider, isObjectSchemaProvider, @@ -22,8 +22,8 @@ import type { IExecutorHandler } from '@cloudbeaver/core-executor'; import { ExtensionUtils } from '@cloudbeaver/core-extensions'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { DATA_CONTEXT_NAV_NODE, EObjectFeature, NodeManagerUtils } from '@cloudbeaver/core-navigation-tree'; -import { ISessionAction, sessionActionContext, SessionActionService } from '@cloudbeaver/core-root'; -import { ACTION_RENAME, ActionService, DATA_CONTEXT_MENU_NESTED, menuExtractItems, MenuService, ViewService } from '@cloudbeaver/core-view'; +import { type ISessionAction, sessionActionContext, SessionActionService } from '@cloudbeaver/core-root'; +import { ACTION_OPEN, ACTION_RENAME, ActionService, menuExtractItems, MenuService, ViewService } from '@cloudbeaver/core-view'; import { MENU_CONNECTIONS } from '@cloudbeaver/plugin-connections'; import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; import { @@ -37,13 +37,15 @@ import { } from '@cloudbeaver/plugin-sql-editor'; import { MENU_APP_ACTIONS } from '@cloudbeaver/plugin-top-app-bar'; -import { ACTION_SQL_EDITOR_NEW } from './ACTION_SQL_EDITOR_NEW'; -import { ACTION_SQL_EDITOR_OPEN } from './ACTION_SQL_EDITOR_OPEN'; -import { DATA_CONTEXT_SQL_EDITOR_TAB } from './DATA_CONTEXT_SQL_EDITOR_TAB'; -import { isSessionActionOpenSQLEditor } from './sessionActionOpenSQLEditor'; -import { SQL_EDITOR_SOURCE_ACTION } from './SQL_EDITOR_SOURCE_ACTION'; -import { SqlEditorNavigatorService } from './SqlEditorNavigatorService'; -import { SqlEditorTabService } from './SqlEditorTabService'; +import { ACTION_SQL_EDITOR_NEW } from './ACTION_SQL_EDITOR_NEW.js'; +import { ACTION_SQL_EDITOR_OPEN } from './ACTION_SQL_EDITOR_OPEN.js'; +import { DATA_CONTEXT_SQL_EDITOR_TAB } from './DATA_CONTEXT_SQL_EDITOR_TAB.js'; +import { isSessionActionOpenSQLEditor } from './sessionActionOpenSQLEditor.js'; +import { SQL_EDITOR_SOURCE_ACTION } from './SQL_EDITOR_SOURCE_ACTION.js'; +import { SqlEditorNavigatorService } from './SqlEditorNavigatorService.js'; +import { SqlEditorTabService } from './SqlEditorTabService.js'; + +const WelcomeNewSqlEditor = importLazyComponent(() => import('./WelcomeNewSqlEditor.js').then(m => m.WelcomeNewSqlEditor)); interface IActiveConnectionContext { connectionKey?: IConnectionInfoParams; @@ -51,7 +53,21 @@ interface IActiveConnectionContext { schemaId?: string; } -@injectable() +@injectable(() => [ + SqlEditorNavigatorService, + NavigationTabsService, + ViewService, + ActionService, + MenuService, + SessionActionService, + CommonDialogService, + SqlEditorTabService, + SqlDataSourceService, + ConnectionInfoResource, + SqlEditorService, + LocalizationService, + SqlEditorSettingsService, +]) export class SqlEditorBootstrap extends Bootstrap { constructor( private readonly sqlEditorNavigatorService: SqlEditorNavigatorService, @@ -71,60 +87,62 @@ export class SqlEditorBootstrap extends Bootstrap { super(); } - register(): void { + override register(): void { + this.navigationTabsService.welcomeContainer.add(WelcomeNewSqlEditor, undefined, () => this.sqlEditorSettingsService.disabled); this.registerTopAppBarItem(); this.menuService.addCreator({ - isApplicable: context => context.has(DATA_CONTEXT_SQL_EDITOR_STATE) && context.has(DATA_CONTEXT_SQL_EDITOR_TAB), + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE, DATA_CONTEXT_SQL_EDITOR_TAB], getItems: (context, items) => [...items, ACTION_RENAME], orderItems: (context, items) => { - const actions = menuExtractItems(items, [ACTION_RENAME]); - - if (actions.length > 0) { - items.unshift(...actions); - } - + items.unshift(...menuExtractItems(items, [ACTION_RENAME])); return items; }, }); this.menuService.addCreator({ + root: true, + contexts: [DATA_CONTEXT_CONNECTION], isApplicable: context => { - if (!context.has(DATA_CONTEXT_CONNECTION)) { - return false; - } - - const node = context.tryGet(DATA_CONTEXT_NAV_NODE); + const node = context.get(DATA_CONTEXT_NAV_NODE); if (node && !node.objectFeatures.includes(EObjectFeature.dataSource)) { return false; } - return !context.has(DATA_CONTEXT_MENU_NESTED); + return true; }, getItems: (context, items) => [...items, ACTION_SQL_EDITOR_OPEN], + orderItems: (context, items) => { + items.unshift(...menuExtractItems(items, [ACTION_OPEN, ACTION_SQL_EDITOR_OPEN])); + return items; + }, }); this.actionService.addHandler({ id: 'sql-editor', isActionApplicable: (context, action) => { - if (action === ACTION_RENAME) { - const editorState = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); + switch (action) { + case ACTION_RENAME: { + const editorState = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); - if (!editorState) { - return false; - } + if (!editorState) { + return false; + } - const dataSource = this.sqlDataSourceService.get(editorState.editorId); + const dataSource = this.sqlDataSourceService.get(editorState.editorId); - return dataSource?.hasFeature(ESqlDataSourceFeatures.setName) ?? false; + return dataSource?.hasFeature(ESqlDataSourceFeatures.setName) ?? false; + } + case ACTION_SQL_EDITOR_OPEN: + return context.has(DATA_CONTEXT_CONNECTION); } - return action === ACTION_SQL_EDITOR_OPEN && context.has(DATA_CONTEXT_CONNECTION); + return false; }, handler: async (context, action) => { switch (action) { case ACTION_RENAME: { - const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(state.editorId); const executionContext = dataSource?.executionContext; @@ -142,7 +160,7 @@ export class SqlEditorBootstrap extends Bootstrap { const regexp = /^(.*?)(\.\w+)$/gi.exec(name); const result = await this.commonDialogService.open(RenameDialog, { - value: regexp?.[1] ?? name, + name: regexp?.[1] ?? name, objectName: name, icon: dataSource.icon, validation: name => @@ -157,11 +175,11 @@ export class SqlEditorBootstrap extends Bootstrap { break; } case ACTION_SQL_EDITOR_OPEN: { - const connection = context.get(DATA_CONTEXT_CONNECTION); + const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!; this.sqlEditorNavigatorService.openNewEditor({ dataSourceKey: LocalStorageSqlDataSource.key, - connectionKey: createConnectionParam(connection), + connectionKey, }); break; } @@ -178,17 +196,14 @@ export class SqlEditorBootstrap extends Bootstrap { }); } - load(): void {} - private registerTopAppBarItem() { this.menuService.addCreator({ menus: [MENU_APP_ACTIONS], getItems: (context, items) => [...items, ACTION_SQL_EDITOR_NEW], orderItems: (context, items) => { - let placeIndex = items.indexOf(ACTION_SQL_EDITOR_NEW); - const actionsOpen = menuExtractItems(items, [ACTION_SQL_EDITOR_NEW]); + let placeIndex = items.indexOf(ACTION_SQL_EDITOR_NEW); const connectionsIndex = items.indexOf(MENU_CONNECTIONS); if (connectionsIndex !== -1) { @@ -203,7 +218,7 @@ export class SqlEditorBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'sql-editor-new', - isActionApplicable: (context, action) => [ACTION_SQL_EDITOR_NEW].includes(action), + actions: [ACTION_SQL_EDITOR_NEW], isLabelVisible: () => false, getActionInfo: (context, action) => { const connectionContext = this.getActiveConnectionContext(); @@ -232,15 +247,15 @@ export class SqlEditorBootstrap extends Bootstrap { }, isHidden: (context, action) => { if (action === ACTION_SQL_EDITOR_NEW) { - return this.sqlEditorSettingsService.settings.getValue('disabled'); + return this.sqlEditorSettingsService.disabled; } return false; }, - handler: (context, action) => { + handler: async (context, action) => { switch (action) { case ACTION_SQL_EDITOR_NEW: { - this.openSQLEditor(); + await this.openSQLEditor(); break; } } @@ -277,10 +292,10 @@ export class SqlEditorBootstrap extends Bootstrap { }; } - private openSQLEditor() { + async openSQLEditor(): Promise { const connectionContext = this.getActiveConnectionContext(); - this.sqlEditorNavigatorService.openNewEditor({ + await this.sqlEditorNavigatorService.openNewEditor({ dataSourceKey: LocalStorageSqlDataSource.key, connectionKey: connectionContext.connectionKey, catalogId: connectionContext.catalogId, diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts index f1893ea1e5..e2b3ce8a17 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorNavigatorService.ts @@ -1,22 +1,28 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { ConnectionInfoResource, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { ConnectionInfoResource, createConnectionParam, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { Executor, IExecutionContextProvider, IExecutor } from '@cloudbeaver/core-executor'; +import { Executor, type IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor'; import { NavigationService } from '@cloudbeaver/core-ui'; import { uuid } from '@cloudbeaver/core-utils'; -import { ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; -import { ISqlEditorTabState, MemorySqlDataSource, SqlDataSourceService, SqlResultTabsService } from '@cloudbeaver/plugin-sql-editor'; - -import { isSQLEditorTab } from './isSQLEditorTab'; -import { SQL_EDITOR_SOURCE_ACTION } from './SQL_EDITOR_SOURCE_ACTION'; -import { SqlEditorTabService } from './SqlEditorTabService'; +import { type ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; +import { + type ISqlEditorTabState, + MemorySqlDataSource, + SqlDataSourceService, + SqlQueryService, + SqlResultTabsService, +} from '@cloudbeaver/plugin-sql-editor'; + +import { isSQLEditorTab } from './isSQLEditorTab.js'; +import { SQL_EDITOR_SOURCE_ACTION } from './SQL_EDITOR_SOURCE_ACTION.js'; +import { SqlEditorTabService } from './SqlEditorTabService.js'; enum SQLEditorNavigationAction { create, @@ -36,6 +42,8 @@ export interface ISQLEditorOptions { schemaId?: string; source?: string; query?: string; + dataSourceState?: Record; + metadata?: Record; } export interface SQLCreateAction extends SQLEditorActionContext, ISQLEditorOptions { @@ -49,7 +57,16 @@ export interface SQLEditorAction extends SQLEditorActionContext { resultId: string; } -@injectable() +@injectable(() => [ + NavigationTabsService, + NotificationService, + SqlEditorTabService, + SqlResultTabsService, + ConnectionInfoResource, + NavigationService, + SqlDataSourceService, + SqlQueryService, +]) export class SqlEditorNavigatorService { private readonly navigator: IExecutor; @@ -61,6 +78,7 @@ export class SqlEditorNavigatorService { private readonly connectionInfoResource: ConnectionInfoResource, navigationService: NavigationService, private readonly sqlDataSourceService: SqlDataSourceService, + private readonly sqlQueryService: SqlQueryService, ) { this.navigator = new Executor(null, (active, current) => active.type === current.type) .before(navigationService.navigationTask) @@ -90,6 +108,16 @@ export class SqlEditorNavigatorService { }); } + async executeEditorQuery(editorId: string, query: string, isNewTab = true): Promise { + const currentTab = this.navigationTabsService.findTab(isSQLEditorTab(tab => tab.id === editorId)); + + if (!currentTab) { + throw new Error(`SQL Editor tab with id "${editorId}" not found.`); + } + + await this.sqlQueryService.executeEditorQuery(currentTab.handlerState, query, isNewTab); + } + private async navigateHandler(data: SQLCreateAction | SQLEditorAction, contexts: IExecutionContextProvider) { try { const tabInfo = contexts.getContext(this.navigationTabsService.navigationTabContext); @@ -125,6 +153,8 @@ export class SqlEditorNavigatorService { data.name, data.source, data.query, + data.dataSourceState, + data.metadata, ); if (tabOptions) { diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorPanel.tsx b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorPanel.tsx index 9f8e5727cd..82d20dee12 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorPanel.tsx +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorPanel.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,15 +10,16 @@ import { observer } from 'mobx-react-lite'; import { DATA_CONTEXT_TAB_ID, useTab } from '@cloudbeaver/core-ui'; import { useCaptureViewContext } from '@cloudbeaver/core-view'; import type { TabHandlerPanelComponent } from '@cloudbeaver/plugin-navigation-tabs'; -import { DATA_CONTEXT_SQL_EDITOR_STATE, ISqlEditorTabState, SqlEditor } from '@cloudbeaver/plugin-sql-editor'; +import { DATA_CONTEXT_SQL_EDITOR_STATE, type ISqlEditorTabState, SqlEditor } from '@cloudbeaver/plugin-sql-editor'; export const SqlEditorPanel: TabHandlerPanelComponent = observer(function SqlEditorPanel({ tab }) { const baseTab = useTab(tab.id); + const handlerState = tab.handlerState; - useCaptureViewContext(context => { + useCaptureViewContext((context, id) => { if (baseTab.selected) { - context?.set(DATA_CONTEXT_TAB_ID, tab.id); - context?.set(DATA_CONTEXT_SQL_EDITOR_STATE, tab.handlerState); + context.set(DATA_CONTEXT_TAB_ID, tab.id, id); + context.set(DATA_CONTEXT_SQL_EDITOR_STATE, handlerState, id); } }); @@ -31,5 +32,5 @@ export const SqlEditorPanel: TabHandlerPanelComponent = obse return null; } - return ; + return ; }); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.m.css b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.m.css deleted file mode 100644 index ddaf5f8410..0000000000 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.m.css +++ /dev/null @@ -1,10 +0,0 @@ -.unsavedMark { - background-color: var(--theme-primary); - width: 8px; - height: 8px; - border-radius: 100%; - margin-right: 4px; - margin-left: -4px; - display: inline-block; - vertical-align: middle; -} diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.module.css b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.module.css new file mode 100644 index 0000000000..5457f168c9 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.module.css @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.unsavedMark { + background-color: var(--theme-primary); + width: 8px; + height: 8px; + border-radius: 100%; + margin-right: 4px; + display: inline-block; + vertical-align: middle; + flex-shrink: 0; +} + +.readonlyIcon { + cursor: auto; + width: 10px; + margin-right: 4px; +} diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx index 7d91a79f06..a7a3194120 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx @@ -1,56 +1,75 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; -import styled from 'reshadow'; -import { s, useStyles } from '@cloudbeaver/core-blocks'; -import { Connection, ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; -import { useDataContext } from '@cloudbeaver/core-data-context'; +import { IconOrImage, s, useObjectInfoTooltip, useTranslate } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; +import { useDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { useService } from '@cloudbeaver/core-di'; -import { ITabData, Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { type ITabData, Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; import { CaptureViewContext } from '@cloudbeaver/core-view'; import type { TabHandlerTabComponent } from '@cloudbeaver/plugin-navigation-tabs'; -import { DATA_CONTEXT_SQL_EDITOR_STATE, getSqlEditorName, ISqlEditorTabState, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor'; +import { + DATA_CONTEXT_SQL_EDITOR_STATE, + ESqlDataSourceFeatures, + getSqlEditorName, + type ISqlEditorTabState, + SqlDataSourceService, +} from '@cloudbeaver/plugin-sql-editor'; -import { DATA_CONTEXT_SQL_EDITOR_TAB } from './DATA_CONTEXT_SQL_EDITOR_TAB'; -import sqlEditorTabStyles from './SqlEditorTab.m.css'; +import { DATA_CONTEXT_SQL_EDITOR_TAB } from './DATA_CONTEXT_SQL_EDITOR_TAB.js'; +import sqlEditorTabStyles from './SqlEditorTab.module.css'; -export const SqlEditorTab: TabHandlerTabComponent = observer(function SqlEditorTab({ tab, onSelect, onClose, style }) { +export const SqlEditorTab: TabHandlerTabComponent = observer(function SqlEditorTab({ tab, onSelect, onClose }) { const viewContext = useContext(CaptureViewContext); const tabMenuContext = useDataContext(viewContext); + const handlerState = tab.handlerState; - tabMenuContext.set(DATA_CONTEXT_SQL_EDITOR_TAB, true); - tabMenuContext.set(DATA_CONTEXT_SQL_EDITOR_STATE, tab.handlerState); + useDataContextLink(tabMenuContext, (context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_TAB, true, id); + context.set(DATA_CONTEXT_SQL_EDITOR_STATE, handlerState, id); + }); const sqlDataSourceService = useService(SqlDataSourceService); const connectionInfo = useService(ConnectionInfoResource); + const projectInfo = useService(ProjectInfoResource); - const dataSource = sqlDataSourceService.get(tab.handlerState.editorId); - let connection: Connection | undefined; - const executionContext = dataSource?.executionContext; + const translate = useTranslate(); - if (executionContext) { - connection = connectionInfo.get(createConnectionParam(executionContext.projectId, executionContext.connectionId)); - } + const dataSource = sqlDataSourceService.get(handlerState.editorId); + const executionContext = dataSource?.executionContext; + const project = executionContext ? projectInfo.get(executionContext.projectId) : undefined; + const connection = executionContext + ? connectionInfo.get(createConnectionParam(executionContext.projectId, executionContext.connectionId)) + : undefined; - const name = getSqlEditorName(tab.handlerState, dataSource, connection); + const name = getSqlEditorName(handlerState, dataSource, connection); const icon = dataSource?.icon ?? '/icons/sql_script_m.svg'; const saved = dataSource?.isSaved !== false; + const isScript = dataSource?.hasFeature(ESqlDataSourceFeatures.script); + const isReadonly = Boolean(dataSource?.isReadonly()); + const hasUnsavedMark = !saved && !isReadonly; const handleSelect = ({ tabId }: ITabData) => onSelect(tabId); const handleClose = onClose ? ({ tabId }: ITabData) => onClose(tabId) : undefined; - return styled(useStyles(style))( - + const tooltip = useObjectInfoTooltip(connection?.name, executionContext?.defaultCatalog, executionContext?.defaultSchema, project?.name); + + return ( + {name} - {!saved && } - , + {isReadonly && isScript && ( + + )} + {hasUnsavedMark &&
} + ); }); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts index 85d73de51b..19793034a2 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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, observable, untracked } from 'mobx'; +import { computed, makeObservable, observable } from 'mobx'; -import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; +import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks'; import { ConnectionExecutionContextResource, ConnectionExecutionContextService, @@ -18,39 +18,56 @@ import { ConnectionsManagerService, ContainerResource, createConnectionParam, - ICatalogData, - IConnectionExecutorData, - IConnectionInfoParams, + executionContextProvider, + type ICatalogData, + type IConnectionExecutorData, + type IConnectionInfoParams, objectCatalogProvider, objectCatalogSetter, + objectLoaderProvider, objectSchemaProvider, objectSchemaSetter, } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { Executor, ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { Executor, ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { NavNodeInfoResource, NodeManagerUtils, objectNavNodeProvider } from '@cloudbeaver/core-navigation-tree'; import { projectProvider, projectSetter, projectSetterState } from '@cloudbeaver/core-projects'; -import { resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { getCachedMapResourceLoaderState, resourceKeyList, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import type { NavNodeInfoFragment } from '@cloudbeaver/core-sdk'; import { isArraysEqual } from '@cloudbeaver/core-utils'; -import { ITab, ITabOptions, NavigationTabsService, TabHandler } from '@cloudbeaver/plugin-navigation-tabs'; +import { type ITab, type ITabOptions, NavigationTabsService, TabHandler } from '@cloudbeaver/plugin-navigation-tabs'; import { ESqlDataSourceFeatures, - ISQLDatasourceUpdateData, - ISqlEditorTabState, + type ISQLDatasourceUpdateData, + type ISqlEditorTabState, + SQL_EDITOR_TAB_STATE_SCHEMA, SqlDataSourceService, SqlEditorService, SqlResultTabsService, } from '@cloudbeaver/plugin-sql-editor'; -import { isSQLEditorTab } from './isSQLEditorTab'; -import { SqlEditorPanel } from './SqlEditorPanel'; -import { SqlEditorTab } from './SqlEditorTab'; -import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey'; +import { isSQLEditorTab } from './isSQLEditorTab.js'; +import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey.js'; -@injectable() +const SqlEditorPanel = importLazyComponent(() => import('./SqlEditorPanel.js').then(m => m.SqlEditorPanel)); +const SqlEditorTab = importLazyComponent(() => import('./SqlEditorTab.js').then(m => m.SqlEditorTab)); + +@injectable(() => [ + NavigationTabsService, + NotificationService, + SqlEditorService, + SqlResultTabsService, + ConnectionExecutionContextService, + ConnectionExecutionContextResource, + ConnectionInfoResource, + NavNodeInfoResource, + SqlDataSourceService, + ConnectionsManagerService, + ContainerResource, + CommonDialogService, +]) export class SqlEditorTabService extends Bootstrap { get sqlEditorTabs(): ITab[] { return Array.from(this.navigationTabsService.findTabs(isSQLEditorTab)); @@ -92,7 +109,9 @@ export class SqlEditorTabService extends Bootstrap { projectProvider(this.getProjectId.bind(this)), connectionProvider(this.getConnectionId.bind(this)), objectCatalogProvider(this.getObjectCatalogId.bind(this)), + objectLoaderProvider(this.getObjectLoader.bind(this)), objectSchemaProvider(this.getObjectSchemaId.bind(this)), + executionContextProvider(this.getExecutionContext.bind(this)), projectSetter(this.setProjectId.bind(this)), connectionSetter((connectionId, tab) => this.setConnectionId(tab, connectionId)), objectCatalogSetter(this.setObjectCatalogId.bind(this)), @@ -107,7 +126,7 @@ export class SqlEditorTabService extends Bootstrap { }); } - register(): void { + override register(): void { this.sqlDataSourceService.onUpdate.addHandler(this.syncDatasourceUpdate.bind(this)); this.connectionsManagerService.onDisconnect.addHandler(this.disconnectHandler.bind(this)); this.connectionInfoResource.onItemDelete.addHandler(this.handleConnectionDelete.bind(this)); @@ -115,14 +134,20 @@ export class SqlEditorTabService extends Bootstrap { this.connectionExecutionContextResource.onItemDelete.addHandler(this.handleExecutionContextDelete.bind(this)); } - load(): void {} - - createNewEditor(editorId: string, dataSourceKey: string, name?: string, source?: string, script?: string): ITabOptions | null { + createNewEditor( + editorId: string, + dataSourceKey: string, + name?: string, + source?: string, + script?: string, + dataSourceState?: Record, + metadata?: Record, + ): ITabOptions | null { const order = this.getFreeEditorId(); - const handlerState = this.sqlEditorService.getState(editorId, dataSourceKey, order, source); + const handlerState = this.sqlEditorService.getState(editorId, dataSourceKey, order, source, metadata); - const datasource = this.sqlDataSourceService.create(handlerState, dataSourceKey, { name, script }); + const datasource = this.sqlDataSourceService.create(handlerState, dataSourceKey, { name, script, dataSourceState }); return { id: editorId, @@ -146,7 +171,17 @@ export class SqlEditorTabService extends Bootstrap { this.attachToProject(tab, null); } - private async handleConnectionDelete(key: ResourceKeySimple) { + getConnectionId(tab: ITab): IConnectionInfoParams | undefined { + const context = this.sqlDataSourceService.get(tab.handlerState.editorId)?.executionContext; + + if (!context) { + return undefined; + } + + return createConnectionParam(context.projectId, context.connectionId); + } + + private handleConnectionDelete(key: ResourceKeySimple) { const tabs = this.navigationTabsService.findTabs( isSQLEditorTab(tab => { const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); @@ -180,9 +215,7 @@ export class SqlEditorTabService extends Bootstrap { const { projectId, connectionId, defaultCatalog, defaultSchema } = executionContext; const connectionKey = createConnectionParam(projectId, connectionId); - const connection = this.connectionInfoResource.get(connectionKey); - - if (!connection?.connected) { + if (!this.connectionInfoResource.isConnected(connectionKey)) { return; } @@ -207,15 +240,13 @@ export class SqlEditorTabService extends Bootstrap { const parents = this.navNodeInfoResource.getParents(nodeId); - untracked(() => this.navNodeInfoResource.load(nodeId!)); - return { nodeId, path: parents, }; } - private async handleExecutionContextUpdate(key: ResourceKeySimple) { + private handleExecutionContextUpdate(key: ResourceKeySimple) { const tabs = this.navigationTabsService.findTabs( isSQLEditorTab(tab => { const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); @@ -244,7 +275,7 @@ export class SqlEditorTabService extends Bootstrap { } } - private async handleExecutionContextDelete(key: ResourceKeySimple) { + private handleExecutionContextDelete(key: ResourceKeySimple) { const tabs = this.navigationTabsService.findTabs( isSQLEditorTab(tab => { const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); @@ -273,20 +304,7 @@ export class SqlEditorTabService extends Bootstrap { } private async handleTabRestore(tab: ITab): Promise { - if ( - typeof tab.handlerState.editorId !== 'string' || - typeof tab.handlerState.editorId !== 'string' || - typeof tab.handlerState.order !== 'number' || - !['string', 'undefined'].includes(typeof tab.handlerState.currentTabId) || - !['string', 'undefined'].includes(typeof tab.handlerState.source) || - !['string', 'undefined'].includes(typeof tab.handlerState.currentModeId) || - !Array.isArray(tab.handlerState.modeState) || - !Array.isArray(tab.handlerState.tabs) || - !Array.isArray(tab.handlerState.executionPlanTabs) || - !Array.isArray(tab.handlerState.resultGroups) || - !Array.isArray(tab.handlerState.resultTabs) || - !Array.isArray(tab.handlerState.statisticsTabs) - ) { + if (!SQL_EDITOR_TAB_STATE_SCHEMA.safeParse(tab.handlerState).success) { await this.sqlDataSourceService.destroy(tab.handlerState.editorId); return false; } @@ -328,14 +346,32 @@ export class SqlEditorTabService extends Bootstrap { return !!dataSource?.hasFeature(ESqlDataSourceFeatures.setProject); } - private getConnectionId(tab: ITab): IConnectionInfoParams | undefined { - const context = this.sqlDataSourceService.get(tab.handlerState.editorId)?.executionContext; + private getObjectLoader(tab: ITab) { + const executionContextComputed = computed(() => this.sqlDataSourceService.get(tab.handlerState.editorId)?.executionContext); - if (!context) { - return undefined; - } + const connectionKeyComputed = computed(() => { + const executionContext = executionContextComputed.get(); - return createConnectionParam(context.projectId, context.connectionId); + if (!executionContext) { + return null; + } + + return createConnectionParam(executionContext.projectId, executionContext.connectionId); + }); + + return [ + getCachedMapResourceLoaderState(this.connectionInfoResource, () => connectionKeyComputed.get()), + getCachedMapResourceLoaderState(this.connectionExecutionContextResource, () => executionContextComputed.get()?.id || null), + getCachedMapResourceLoaderState(this.containerResource, () => connectionKeyComputed.get()), + // TODO: maybe we need it for this.getNavNode to work properly, but it's seems working without it + // getCachedMapResourceLoaderState(this.navNodeInfoResource, () => { + // if (this.containerResource.isLoadable(connectionKey)) { + // return null; + // } + // console.log('node:', this.getNavNode(tab)?.nodeId || null); + // return this.getNavNode(tab)?.nodeId || null; + // }), + ]; } private getObjectCatalogId(tab: ITab) { @@ -350,6 +386,11 @@ export class SqlEditorTabService extends Bootstrap { return context?.defaultSchema; } + private getExecutionContext(tab: ITab) { + const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); + return dataSource?.executionContext; + } + private setProjectId(projectId: string | null, tab: ITab): boolean { const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); @@ -417,7 +458,7 @@ export class SqlEditorTabService extends Bootstrap { } } - private async syncDatasourceUpdate(data: ISQLDatasourceUpdateData) { + private syncDatasourceUpdate(data: ISQLDatasourceUpdateData) { const tab = this.sqlEditorTabs.find(tab => tab.handlerState.editorId === data.editorId); if (tab) { @@ -466,7 +507,7 @@ export class SqlEditorTabService extends Bootstrap { const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId); - if (dataSource?.isSaved === false) { + if (dataSource?.isSaved === false && !dataSource?.isReadonly()) { const result = await this.commonDialogService.open(ConfirmationDialog, { title: 'plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', subTitle: dataSource.name ?? undefined, diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/WelcomeNewSqlEditor.tsx b/webapp/packages/plugin-sql-editor-navigation-tab/src/WelcomeNewSqlEditor.tsx new file mode 100644 index 0000000000..070a7f2a1f --- /dev/null +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/WelcomeNewSqlEditor.tsx @@ -0,0 +1,30 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { Cell, IconOrImage, useTranslate } from '@cloudbeaver/core-blocks'; +import { observer } from 'mobx-react-lite'; +import { useService } from '@cloudbeaver/core-di'; +import { SqlEditorBootstrap } from './SqlEditorBootstrap.js'; +import { ACTION_SQL_EDITOR_NEW } from './ACTION_SQL_EDITOR_NEW.js'; + +export const WelcomeNewSqlEditor = observer(function WelcomeNewSqlEditor() { + const sqlEditorBootstrap = useService(SqlEditorBootstrap); + const translate = useTranslate(); + return ( + + ); +}); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/index.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/index.ts index deb0d8e027..b20ed60f30 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/index.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/index.ts @@ -1,10 +1,19 @@ -import { sqlEditorTabPluginManifest } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { sqlEditorTabPluginManifest } from './manifest.js'; export default sqlEditorTabPluginManifest; -export { DATA_CONTEXT_SQL_EDITOR_TAB } from './DATA_CONTEXT_SQL_EDITOR_TAB'; -export { ACTION_SQL_EDITOR_OPEN } from './ACTION_SQL_EDITOR_OPEN'; +export { DATA_CONTEXT_SQL_EDITOR_TAB } from './DATA_CONTEXT_SQL_EDITOR_TAB.js'; +export { ACTION_SQL_EDITOR_OPEN } from './ACTION_SQL_EDITOR_OPEN.js'; -export * from './isSQLEditorTab'; -export * from './SqlEditorNavigatorService'; -export * from './SqlEditorTabService'; +export * from './isSQLEditorTab.js'; +export * from './SqlEditorNavigatorService.js'; +export * from './SqlEditorTabService.js'; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/isSQLEditorTab.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/isSQLEditorTab.ts index 51e08eb76f..dd7a6299ea 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/isSQLEditorTab.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/isSQLEditorTab.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,7 +8,7 @@ import type { ITab } from '@cloudbeaver/plugin-navigation-tabs'; import type { ISqlEditorTabState } from '@cloudbeaver/plugin-sql-editor'; -import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey'; +import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey.js'; export function isSQLEditorTab(tab: ITab): tab is ITab; export function isSQLEditorTab(predicate: (tab: ITab) => boolean): (tab: ITab) => tab is ITab; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/fr.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/fr.ts new file mode 100644 index 0000000000..8b0139fd71 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/fr.ts @@ -0,0 +1,7 @@ +export default [ + ['plugin_sql_editor_navigation_tab_action_sql_editor_new', 'SQL'], + ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', "Ouvrir l'éditeur SQL"], + ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', "Ouvrir l'éditeur SQL pour {arg:connection}"], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', 'Enregistrer les modifications'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', 'Voulez-vous enregistrer les modifications ?'], +]; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/vi.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/vi.ts new file mode 100644 index 0000000000..d07b03ffbc --- /dev/null +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/vi.ts @@ -0,0 +1,7 @@ +export default [ + ['plugin_sql_editor_navigation_tab_action_sql_editor_new', 'SQL'], + ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', 'Mở Trình Soạn Thảo SQL'], + ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', 'Mở Trình Soạn Thảo SQL cho {arg:connection}'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', 'Lưu thay đổi'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', 'Bạn có muốn lưu thay đổi không?'], +]; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/zh.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/zh.ts index aa3d5ae2ce..9b27b79446 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/zh.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/zh.ts @@ -1,7 +1,7 @@ export default [ ['plugin_sql_editor_navigation_tab_action_sql_editor_new', 'SQL'], - ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', 'Open SQL Editor'], - ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', 'Open SQL Editor for {arg:connection}'], - ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', 'Save changes'], - ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', 'Do you want to save changes?'], + ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', '打开SQL编辑器'], + ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', '为 {arg:connection} 打开SQL编辑器'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', '保存更改'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', '是否保存更改?'], ]; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/manifest.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/manifest.ts index c4ad28c772..0b449632ff 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/manifest.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/manifest.ts @@ -1,21 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { LocaleService } from './LocaleService'; -import { SqlEditorBootstrap } from './SqlEditorBootstrap'; -import { SqlEditorNavigatorService } from './SqlEditorNavigatorService'; -import { SqlEditorTabService } from './SqlEditorTabService'; - export const sqlEditorTabPluginManifest: PluginManifest = { info: { name: 'Sql Editor Navigation Tab Plugin', }, - - providers: [SqlEditorBootstrap, SqlEditorTabService, SqlEditorNavigatorService, LocaleService], }; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/module.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/module.ts new file mode 100644 index 0000000000..587c07b580 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/module.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { SqlEditorTabService } from './SqlEditorTabService.js'; +import { SqlEditorNavigatorService } from './SqlEditorNavigatorService.js'; +import { SqlEditorBootstrap } from './SqlEditorBootstrap.js'; +import { LocaleService } from './LocaleService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-sql-editor-navigation-tab', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, LocaleService) + .addSingleton(Bootstrap, proxy(SqlEditorBootstrap)) + .addSingleton(Bootstrap, proxy(SqlEditorTabService)) + .addSingleton(SqlEditorTabService) + .addSingleton(SqlEditorBootstrap) + .addSingleton(SqlEditorNavigatorService); + }, +}); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/sessionActionOpenSQLEditor.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/sessionActionOpenSQLEditor.ts index 1bb4f01c7d..a2f1857fcc 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/sessionActionOpenSQLEditor.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/sessionActionOpenSQLEditor.ts @@ -1,13 +1,13 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { ISessionAction } from '@cloudbeaver/core-root'; -import { SESSION_ACTION_OPEN_SQL_EDITOR } from './SESSION_ACTION_OPEN_SQL_EDITOR'; +import { SESSION_ACTION_OPEN_SQL_EDITOR } from './SESSION_ACTION_OPEN_SQL_EDITOR.js'; export interface ISessionActionOpenSQLEditor { action: typeof SESSION_ACTION_OPEN_SQL_EDITOR; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/sqlEditorTabHandlerKey.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/sqlEditorTabHandlerKey.ts index a37ea5c51d..7de2beef6b 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/sqlEditorTabHandlerKey.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/sqlEditorTabHandlerKey.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/tsconfig.json b/webapp/packages/plugin-sql-editor-navigation-tab/tsconfig.json index 805ec02fc1..ec21f96030 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/tsconfig.json +++ b/webapp/packages/plugin-sql-editor-navigation-tab/tsconfig.json @@ -1,73 +1,77 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-connections/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../plugin-navigation-tabs/tsconfig.json" + "path": "../core-cli" }, { - "path": "../plugin-sql-editor/tsconfig.json" + "path": "../core-connections" }, { - "path": "../plugin-top-app-bar/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-dialogs" }, { - "path": "../core-data-context/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-executor" }, { - "path": "../core-dialogs/tsconfig.json" + "path": "../core-extensions" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-executor/tsconfig.json" + "path": "../core-navigation-tree" }, { - "path": "../core-extensions/tsconfig.json" + "path": "../core-projects" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-navigation-tree/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-projects/tsconfig.json" + "path": "../core-sdk" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-utils" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-view" }, { - "path": "../core-ui/tsconfig.json" + "path": "../plugin-connections" }, { - "path": "../core-utils/tsconfig.json" + "path": "../plugin-navigation-tabs" }, { - "path": "../core-view/tsconfig.json" + "path": "../plugin-sql-editor" + }, + { + "path": "../plugin-top-app-bar" } ], "include": [ @@ -79,7 +83,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-sql-editor-new/.gitignore b/webapp/packages/plugin-sql-editor-new/.gitignore index 72ea21f191..4ef7eb1488 100644 --- a/webapp/packages/plugin-sql-editor-new/.gitignore +++ b/webapp/packages/plugin-sql-editor-new/.gitignore @@ -5,8 +5,8 @@ /coverage # production -!lib/ -!lib/lib +lib +dist # misc .DS_Store diff --git a/webapp/packages/plugin-sql-editor-new/package.json b/webapp/packages/plugin-sql-editor-new/package.json index d4c1a02f33..5c0bdda2e4 100644 --- a/webapp/packages/plugin-sql-editor-new/package.json +++ b/webapp/packages/plugin-sql-editor-new/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-sql-editor-new", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,26 +11,37 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-codemirror6": "~0.1.0", - "@cloudbeaver/plugin-sql-editor": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-navigation-tree": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/plugin-codemirror6": "workspace:*", + "@cloudbeaver/plugin-sql-editor": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "peerDependencies": {}, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } } diff --git a/webapp/packages/plugin-sql-editor-new/src/LocaleService.ts b/webapp/packages/plugin-sql-editor-new/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-sql-editor-new/src/LocaleService.ts +++ b/webapp/packages/plugin-sql-editor-new/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-sql-editor-new/src/PluginBootstrap.ts b/webapp/packages/plugin-sql-editor-new/src/PluginBootstrap.ts index 82f5329d89..e830f3ae20 100644 --- a/webapp/packages/plugin-sql-editor-new/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-new/src/PluginBootstrap.ts @@ -1,23 +1,21 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { SQLCodeEditorPanelService } from './SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService'; +import { SQLCodeEditorPanelService } from './SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.js'; -@injectable() +@injectable(() => [SQLCodeEditorPanelService]) export class PluginBootstrap extends Bootstrap { constructor(private readonly sqlCodeEditorPanelService: SQLCodeEditorPanelService) { super(); } - register(): void | Promise { + override register(): void | Promise { this.sqlCodeEditorPanelService.registerPanel(); } - - load(): void | Promise {} } diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/ACTIVE_QUERY_EXTENSION.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/ACTIVE_QUERY_EXTENSION.ts index 13e2eb0d33..6ef634e413 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/ACTIVE_QUERY_EXTENSION.ts +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/ACTIVE_QUERY_EXTENSION.ts @@ -1,11 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { Decoration, DecorationSet, EditorView, StateEffect, StateField } from '@cloudbeaver/plugin-codemirror6'; +import { Decoration, type DecorationSet, EditorView, StateEffect, StateField } from '@cloudbeaver/plugin-codemirror6'; const ACTIVE_QUERY_EFFECT_ADD = StateEffect.define<{ from: number; to: number | undefined }>({ map: (val, mapping) => ({ diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/QUERY_STATUS_GUTTER_EXTENSION.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/QUERY_STATUS_GUTTER_EXTENSION.ts index 72e0962f46..a47a179218 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/QUERY_STATUS_GUTTER_EXTENSION.ts +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/QUERY_STATUS_GUTTER_EXTENSION.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ type QueryGutterEffectType = 'run' | 'error'; const QUERY_STATUS_SIZE_MARKER = new (class extends GutterMarker {})(); const RUN_QUERY_MARKER = new (class extends GutterMarker { - toDOM(): Node { + override toDOM(): Node { const span = document.createElement('div'); span.className = 'running-query-line'; return span; @@ -20,7 +20,7 @@ const RUN_QUERY_MARKER = new (class extends GutterMarker { })(); const ERROR_QUERY_MARKER = new (class extends GutterMarker { - toDOM(): Node { + override toDOM(): Node { const span = document.createElement('div'); span.className = 'running-query-error-line'; return span; @@ -31,12 +31,13 @@ const QUERY_GUTTER_EFFECT = StateEffect.define<{ pos: number; on: boolean; type: map: (val, mapping) => ({ pos: mapping.mapPos(val.pos), on: val.on, type: val.type }), }); -const gutterExtension = StateField.define>({ +const statusGutterState = StateField.define>({ create() { return RangeSet.empty; }, update(set, transaction) { set = set.map(transaction.changes); + for (const effect of transaction.effects) { if (effect.is(QUERY_GUTTER_EFFECT)) { if (effect.value.on) { @@ -68,10 +69,10 @@ export function setGutter(view: EditorView, pos: number, type: QueryGutterEffect } export const QUERY_STATUS_GUTTER_EXTENSION = [ - gutterExtension, + statusGutterState, gutter({ class: 'query-status', - markers: view => view.state.field(gutterExtension), + markers: view => view.state.field(statusGutterState), initialSpacer: () => QUERY_STATUS_SIZE_MARKER, }), ]; diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditor.tsx b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditor.tsx index d8597e2bff..2177b1b8b4 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditor.tsx +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditor.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { forwardRef } from 'react'; -import { EditorLoader, IDefaultExtensions, IEditorProps, IEditorRef } from '@cloudbeaver/plugin-codemirror6'; +import { EditorLoader, type IDefaultExtensions, type IEditorProps, type IEditorRef } from '@cloudbeaver/plugin-codemirror6'; export const SQLCodeEditor = observer( forwardRef(function SQLCodeEditor(props, ref) { diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditorLoader.tsx b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditorLoader.tsx index 70eb823a4c..deb626b62d 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditorLoader.tsx +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/SQLCodeEditorLoader.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ import { ComplexLoader, createComplexLoader } from '@cloudbeaver/core-blocks'; import type { IDefaultExtensions, IEditorProps, IEditorRef } from '@cloudbeaver/plugin-codemirror6'; const loader = createComplexLoader(async function loader() { - const { SQLCodeEditor } = await import('./SQLCodeEditor'); + const { SQLCodeEditor } = await import('./SQLCodeEditor.js'); return { SQLCodeEditor }; }); diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/useSQLCodeEditor.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/useSQLCodeEditor.ts index 429267e6b1..d0dc7b8967 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/useSQLCodeEditor.ts +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditor/useSQLCodeEditor.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -10,8 +10,8 @@ import { observable } from 'mobx'; import { useObservableRef } from '@cloudbeaver/core-blocks'; import type { EditorState, EditorView, IEditorRef } from '@cloudbeaver/plugin-codemirror6'; -import { clearActiveQueryHighlight, highlightActiveQuery } from '../ACTIVE_QUERY_EXTENSION'; -import { setGutter } from '../QUERY_STATUS_GUTTER_EXTENSION'; +import { clearActiveQueryHighlight, highlightActiveQuery } from '../ACTIVE_QUERY_EXTENSION.js'; +import { setGutter } from '../QUERY_STATUS_GUTTER_EXTENSION.js'; export interface IEditor { readonly view: EditorView | null; diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.m.css b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.m.css deleted file mode 100644 index 71a4d47d6d..0000000000 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.m.css +++ /dev/null @@ -1,10 +0,0 @@ -.reactCodemirrorPanel { - composes: theme-typography--caption from global; - display: flex; - padding: 2px; -} -.box { - display: flex; - flex: 1; - overflow: auto; -} diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.module.css b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.module.css new file mode 100644 index 0000000000..a73ee4def4 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.module.css @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.reactCodemirrorPanel { + composes: theme-typography--caption from global; + display: flex; + padding: 2px; +} +.box { + display: flex; + flex: 1; + overflow: auto; +} diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx index ca5a31ba61..12a6285dcb 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanel.tsx @@ -1,60 +1,41 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useEffect, useState } from 'react'; +import { useState } from 'react'; import { MenuBarSmallItem, useExecutor, useS, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { DATA_CONTEXT_NAV_NODE, getNodesFromContext, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; -import { TabContainerPanelComponent, useDNDBox, useTabLocalState } from '@cloudbeaver/core-ui'; -import { closeCompletion, IEditorRef, Prec, ReactCodemirrorPanel, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; +import { type TabContainerPanelComponent, useDNDBox } from '@cloudbeaver/core-ui'; +import { closeCompletion, type IEditorRef, Prec, ReactCodemirrorPanel, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; import type { ISqlEditorModeProps } from '@cloudbeaver/plugin-sql-editor'; -import { ACTIVE_QUERY_EXTENSION } from '../ACTIVE_QUERY_EXTENSION'; -import { QUERY_STATUS_GUTTER_EXTENSION } from '../QUERY_STATUS_GUTTER_EXTENSION'; -import { SQLCodeEditorLoader } from '../SQLCodeEditor/SQLCodeEditorLoader'; -import { useSQLCodeEditor } from '../SQLCodeEditor/useSQLCodeEditor'; -import { useSqlDialectAutocompletion } from '../useSqlDialectAutocompletion'; -import { useSqlDialectExtension } from '../useSqlDialectExtension'; -import style from './SQLCodeEditorPanel.m.css'; -import { useSQLCodeEditorPanel } from './useSQLCodeEditorPanel'; - -interface ILocalSQLCodeEditorPanelState { - selection: { from: number; to: number }; -}; +import { ACTIVE_QUERY_EXTENSION } from '../ACTIVE_QUERY_EXTENSION.js'; +import { QUERY_STATUS_GUTTER_EXTENSION } from '../QUERY_STATUS_GUTTER_EXTENSION.js'; +import { SQLCodeEditorLoader } from '../SQLCodeEditor/SQLCodeEditorLoader.js'; +import { useSQLCodeEditor } from '../SQLCodeEditor/useSQLCodeEditor.js'; +import { useSqlDialectAutocompletion } from '../useSqlDialectAutocompletion.js'; +import { useSqlDialectExtension } from '../useSqlDialectExtension.js'; +import style from './SQLCodeEditorPanel.module.css'; +import { SqlEditorInfoBar } from './SqlEditorInfoBar.js'; +import { useSQLCodeEditorPanel } from './useSQLCodeEditorPanel.js'; + export const SQLCodeEditorPanel: TabContainerPanelComponent = observer(function SQLCodeEditorPanel({ data }) { const notificationService = useService(NotificationService); const navNodeManagerService = useService(NavNodeManagerService); const translate = useTranslate(); - const localState = useTabLocalState(() => ({ selection: { from: 0, to: 0 } })); const styles = useS(style); const [editorRef, setEditorRef] = useState(null); const editor = useSQLCodeEditor(editorRef); - useEffect(() => { - - editorRef?.view?.dispatch({ - selection: { anchor: localState.selection.from, head: localState.selection.to }, - scrollIntoView: true, - }); - }, [editorRef?.view, localState]); - - useEffect(() => { - if (!editorRef?.selection) { - return; - } - - localState.selection = { ...editorRef?.selection }; - }, [editorRef?.selection]); - const panel = useSQLCodeEditorPanel(data, editor); const extensions = useCodemirrorExtensions(undefined, [ACTIVE_QUERY_EXTENSION, Prec.lowest(QUERY_STATUS_GUTTER_EXTENSION)]); const autocompletion = useSqlDialectAutocompletion(data); @@ -73,26 +54,19 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent try { const pos = view.posAtCoords({ x: mouse.x, y: mouse.y }) ?? 1; - await data.executeQueryAction( - data.cursorSegment, - async () => { - const alias: string[] = []; - - for (const node of nodes) { - alias.push(await navNodeManagerService.getNodeDatabaseAlias(node.id)); - } - - const replacement = alias.join(', '); - if (replacement) { - view.dispatch({ - changes: { from: pos, to: pos, insert: replacement }, - selection: { anchor: pos, head: pos + replacement.length }, - }); - } - }, - true, - true, - ); + const alias: string[] = []; + + for (const node of nodes) { + alias.push(await navNodeManagerService.getNodeDatabaseAlias(node.id)); + } + + const replacement = alias.join(', '); + if (replacement) { + view.dispatch({ + changes: { from: pos, to: pos, insert: replacement }, + selection: { anchor: pos, head: pos + replacement.length }, + }); + } } catch (exception: any) { notificationService.logException(exception, 'sql_editor_alias_loading_error'); } @@ -120,32 +94,41 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent } return ( -
+
data.value} + cursor={{ + anchor: data.cursor.anchor, + head: data.cursor.head, + }} incomingValue={data.incomingValue} extensions={extensions} readonly={data.readonly} autoFocus lineNumbers onChange={panel.onQueryChange} - onUpdate={panel.onUpdate} + onCursorChange={selection => panel.onCursorChange(selection.anchor, selection.head)} > {data.isIncomingChanges && ( <> - + {translate('plugin_sql_editor_new_merge_conflict_keep_current_label')} - + {translate('plugin_sql_editor_new_merge_conflict_accept_incoming_label')} )} + {editor.state && ( + + + + )}
); diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts index 2cee115fa3..e53bc5c888 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.ts @@ -1,21 +1,17 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import React from 'react'; - +import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; import { ESqlDataSourceFeatures, SqlEditorModeService } from '@cloudbeaver/plugin-sql-editor'; -const SQLCodeEditorPanel = React.lazy(async () => { - const { SQLCodeEditorPanel } = await import('./SQLCodeEditorPanel'); - return { default: SQLCodeEditorPanel }; -}); +const SQLCodeEditorPanel = importLazyComponent(() => import('./SQLCodeEditorPanel.js').then(module => module.SQLCodeEditorPanel)); -@injectable() +@injectable(() => [SqlEditorModeService]) export class SQLCodeEditorPanelService { constructor(private readonly sqlEditorModeService: SqlEditorModeService) {} diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SqlEditorInfoBar.module.css b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SqlEditorInfoBar.module.css new file mode 100644 index 0000000000..734a498fbe --- /dev/null +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SqlEditorInfoBar.module.css @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.container { + display: flex; + align-items: center; + padding: 0 6px; +} +.info { + composes: theme-typography--caption from global; + margin-left: auto; +} diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SqlEditorInfoBar.tsx b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SqlEditorInfoBar.tsx new file mode 100644 index 0000000000..ebc3a5c8b0 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/SqlEditorInfoBar.tsx @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { s, useS } from '@cloudbeaver/core-blocks'; +import type { EditorState } from '@cloudbeaver/plugin-codemirror6'; + +import classes from './SqlEditorInfoBar.module.css'; + +interface Props { + state: EditorState; +} + +export const SqlEditorInfoBar = observer(function SqlEditorInfoBar({ state }) { + const styles = useS(classes); + + const cursorPos = state.selection.main.head; + const line = state.doc.lineAt(cursorPos); + // We need to add 1 to the cursor index because the cursor is zero-based + const cursorIndexInRow = cursorPos - line.from + 1; + + return ( +
+
{`Ln ${line.number}, Col ${cursorIndexInRow}, Pos ${cursorPos}`}
+
+ ); +}); diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts index c36ac3943d..1c503cdda7 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/SQLCodeEditorPanel/useSQLCodeEditorPanel.ts @@ -1,25 +1,27 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action } from 'mobx'; import { useCallback } from 'react'; import { useExecutor, useObservableRef } from '@cloudbeaver/core-blocks'; import { throttle } from '@cloudbeaver/core-utils'; -import type { Transaction, ViewUpdate } from '@cloudbeaver/plugin-codemirror6'; -import type { ISQLEditorData } from '@cloudbeaver/plugin-sql-editor'; +import type { ISqlEditorCursor, ISQLEditorData } from '@cloudbeaver/plugin-sql-editor'; -import type { IEditor } from '../SQLCodeEditor/useSQLCodeEditor'; +import type { IEditor } from '../SQLCodeEditor/useSQLCodeEditor.js'; interface State { highlightActiveQuery: () => void; - onQueryChange: (query: string) => void; - onUpdate: (update: ViewUpdate) => void; + onQueryChange: (query: string, selection: ISqlEditorCursor) => void; + onCursorChange: (anchor: number, head?: number) => void; } +export const ON_QUERY_CHANGE_SOURCE = 'QueryChange'; + export function useSQLCodeEditorPanel(data: ISQLEditorData, editor: IEditor) { const state: State = useObservableRef( () => ({ @@ -32,24 +34,16 @@ export function useSQLCodeEditorPanel(data: ISQLEditorData, editor: IEditor) { this.editor.highlightActiveQuery(segment.begin, segment.end); } }, - onQueryChange(query: string) { - this.data.setQuery(query); + onQueryChange(query: string, selection: ISqlEditorCursor) { + this.data.setScript(query, ON_QUERY_CHANGE_SOURCE, selection); + this.onCursorChange(selection.anchor, selection.head); }, - onUpdate(update: ViewUpdate) { - const transactions = update.transactions.filter(t => t.selection !== undefined); - const lastTransaction = transactions[transactions.length - 1] as Transaction | undefined; - - if (lastTransaction) { - const from = lastTransaction.selection?.main.from ?? update.state.selection.main.from; - const to = lastTransaction.selection?.main.to ?? update.state.selection.main.to; - - this.data.setCursor(from, to); - } + onCursorChange(anchor: number, head?: number) { + this.data.setCursor(anchor, head); }, }), - {}, + { onQueryChange: action.bound, onCursorChange: action.bound }, { editor, data }, - ['onQueryChange', 'onUpdate'], ); const updateHighlight = useCallback( diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectAutocompletion.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectAutocompletion.ts index f0b639908f..3360c31732 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectAutocompletion.ts +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectAutocompletion.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ import { useService } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { GlobalConstants } from '@cloudbeaver/core-utils'; import type { Compartment, Completion, CompletionConfig, CompletionContext, CompletionResult, Extension } from '@cloudbeaver/plugin-codemirror6'; -import type { ISQLEditorData, SQLProposal } from '@cloudbeaver/plugin-sql-editor'; +import { type ISQLEditorData, type SQLProposal } from '@cloudbeaver/plugin-sql-editor'; const codemirrorComplexLoader = createComplexLoader(() => import('@cloudbeaver/plugin-codemirror6')); @@ -24,19 +24,31 @@ const CLOSE_CHARACTERS = /[\s()[\]{};:>,=\\*]/; const COMPLETION_WORD = /[\w*]*/; export function useSqlDialectAutocompletion(data: ISQLEditorData): [Compartment, Extension] { - const { closeCompletion, useEditorAutocompletion } = useComplexLoader(codemirrorComplexLoader); + const { closeCompletion, useEditorAutocompletion, insertCompletionText } = useComplexLoader(codemirrorComplexLoader); const localizationService = useService(LocalizationService); const optionsRef = useObjectRef({ data }); const [config] = useState(() => { function getOptionsFromProposals(explicit: boolean, word: string, proposals: SQLProposal[]): SqlCompletion[] { const wordLowerCase = word.toLocaleLowerCase(); - const hasSameName = proposals.some(({ displayString }) => displayString.toLocaleLowerCase() === wordLowerCase); + const hasSameName = proposals.some( + ({ replacementString, displayString }) => + sanitizeProposal(displayString) === wordLowerCase || replacementString.toLocaleLowerCase() === wordLowerCase, + ); const filteredProposals = proposals - .filter( - ({ displayString }) => - word === '*' || (displayString.toLocaleLowerCase() !== wordLowerCase && displayString.toLocaleLowerCase().startsWith(wordLowerCase)), - ) + .filter(({ replacementString, displayString }) => { + if (word === '*') { + return true; + } + + const display = sanitizeProposal(displayString); + const replacement = replacementString.toLocaleLowerCase(); + + const displayMatch = display !== wordLowerCase && (display.startsWith(wordLowerCase) || display.includes(wordLowerCase)); + const replacementMatch = replacement !== wordLowerCase && (replacement.startsWith(wordLowerCase) || replacement.includes(wordLowerCase)); + + return displayMatch || replacementMatch; + }) .sort((a, b) => (b.score ?? 0) - (a.score ?? 0)); if (filteredProposals.length === 0 && !hasSameName && explicit) { @@ -51,7 +63,9 @@ export function useSqlDialectAutocompletion(data: ISQLEditorData): [Compartment, return [ ...filteredProposals.map(proposal => ({ label: proposal.displayString, - apply: proposal.replacementString, + apply: (view, completion, from, to) => { + view.dispatch(insertCompletionText(view.state, proposal.replacementString, proposal.replacementOffset, to)); + }, boost: proposal.score, icon: proposal.icon, })), @@ -142,3 +156,7 @@ export function useSqlDialectAutocompletion(data: ISQLEditorData): [Compartment, return useEditorAutocompletion(config); } + +function sanitizeProposal(value: string): string { + return value.replace(/^"|"$/g, '').toLocaleLowerCase(); +} diff --git a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectExtension.ts b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectExtension.ts index dc36668e0c..26a4665fb0 100644 --- a/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectExtension.ts +++ b/webapp/packages/plugin-sql-editor-new/src/SQLEditor/useSqlDialectExtension.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-sql-editor-new/src/index.ts b/webapp/packages/plugin-sql-editor-new/src/index.ts index 1871f16af4..ba870f16b5 100644 --- a/webapp/packages/plugin-sql-editor-new/src/index.ts +++ b/webapp/packages/plugin-sql-editor-new/src/index.ts @@ -1,7 +1,16 @@ -import { sqlEditorNewPlugin } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { sqlEditorNewPlugin } from './manifest.js'; export default sqlEditorNewPlugin; -export * from './SQLEditor/SQLCodeEditor/SQLCodeEditorLoader'; -export * from './SQLEditor/useSqlDialectAutocompletion'; -export * from './SQLEditor/useSqlDialectExtension'; +export * from './SQLEditor/SQLCodeEditor/SQLCodeEditorLoader.js'; +export * from './SQLEditor/useSqlDialectAutocompletion.js'; +export * from './SQLEditor/useSqlDialectExtension.js'; diff --git a/webapp/packages/plugin-sql-editor-new/src/locales/fr.ts b/webapp/packages/plugin-sql-editor-new/src/locales/fr.ts new file mode 100644 index 0000000000..d619b5e952 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-new/src/locales/fr.ts @@ -0,0 +1,6 @@ +export default [ + ['plugin_sql_editor_new_merge_conflict_keep_current_label', "Garder l'actuel"], + ['plugin_sql_editor_new_merge_conflict_keep_current_tooltip', 'Garder les modifications actuelles'], + ['plugin_sql_editor_new_merge_conflict_accept_incoming_label', 'Accepter les modifications entrantes'], + ['plugin_sql_editor_new_merge_conflict_accept_incoming_tooltip', 'Accepter les modifications entrantes'], +]; diff --git a/webapp/packages/plugin-sql-editor-new/src/locales/vi.ts b/webapp/packages/plugin-sql-editor-new/src/locales/vi.ts new file mode 100644 index 0000000000..a992adf767 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-new/src/locales/vi.ts @@ -0,0 +1,6 @@ +export default [ + ['plugin_sql_editor_new_merge_conflict_keep_current_label', 'Giữ Hiện tại'], + ['plugin_sql_editor_new_merge_conflict_keep_current_tooltip', 'Giữ các thay đổi Hiện tại'], + ['plugin_sql_editor_new_merge_conflict_accept_incoming_label', 'Chấp nhận Thay đổi Mới'], + ['plugin_sql_editor_new_merge_conflict_accept_incoming_tooltip', 'Chấp nhận các Thay đổi Mới'], +]; diff --git a/webapp/packages/plugin-sql-editor-new/src/locales/zh.ts b/webapp/packages/plugin-sql-editor-new/src/locales/zh.ts index 36472b6f37..1d612c81e7 100644 --- a/webapp/packages/plugin-sql-editor-new/src/locales/zh.ts +++ b/webapp/packages/plugin-sql-editor-new/src/locales/zh.ts @@ -1,6 +1,6 @@ export default [ - ['plugin_sql_editor_new_merge_conflict_keep_current_label', 'Keep Current'], - ['plugin_sql_editor_new_merge_conflict_keep_current_tooltip', 'Keep Current changes'], - ['plugin_sql_editor_new_merge_conflict_accept_incoming_label', 'Accept Incoming'], - ['plugin_sql_editor_new_merge_conflict_accept_incoming_tooltip', 'Accept Incoming changes'], + ['plugin_sql_editor_new_merge_conflict_keep_current_label', '保留当前'], + ['plugin_sql_editor_new_merge_conflict_keep_current_tooltip', '保留当前改动'], + ['plugin_sql_editor_new_merge_conflict_accept_incoming_label', '接受传入'], + ['plugin_sql_editor_new_merge_conflict_accept_incoming_tooltip', '接受传入改动'], ]; diff --git a/webapp/packages/plugin-sql-editor-new/src/manifest.ts b/webapp/packages/plugin-sql-editor-new/src/manifest.ts index de99ef493f..f5e27c7649 100644 --- a/webapp/packages/plugin-sql-editor-new/src/manifest.ts +++ b/webapp/packages/plugin-sql-editor-new/src/manifest.ts @@ -1,20 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { LocaleService } from './LocaleService'; -import { PluginBootstrap } from './PluginBootstrap'; -import { SQLCodeEditorPanelService } from './SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService'; - export const sqlEditorNewPlugin: PluginManifest = { info: { name: 'Sql Editor New Plugin', }, - - providers: [PluginBootstrap, SQLCodeEditorPanelService, LocaleService], }; diff --git a/webapp/packages/plugin-sql-editor-new/src/module.ts b/webapp/packages/plugin-sql-editor-new/src/module.ts new file mode 100644 index 0000000000..a94fb340c8 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-new/src/module.ts @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, ModuleRegistry } from '@cloudbeaver/core-di'; +import { PluginBootstrap } from './PluginBootstrap.js'; +import { SQLCodeEditorPanelService } from './SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService.js'; +import { LocaleService } from './LocaleService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-sql-editor-new', + + configure: serviceCollection => { + serviceCollection.addSingleton(Bootstrap, LocaleService).addSingleton(Bootstrap, PluginBootstrap).addSingleton(SQLCodeEditorPanelService); + }, +}); diff --git a/webapp/packages/plugin-sql-editor-new/tsconfig.json b/webapp/packages/plugin-sql-editor-new/tsconfig.json index 18eb29a80a..54e25c144d 100644 --- a/webapp/packages/plugin-sql-editor-new/tsconfig.json +++ b/webapp/packages/plugin-sql-editor-new/tsconfig.json @@ -1,40 +1,44 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-codemirror6/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../plugin-sql-editor/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-navigation-tree" }, { - "path": "../core-navigation-tree/tsconfig.json" + "path": "../core-sdk" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-utils" }, { - "path": "../core-utils/tsconfig.json" + "path": "../plugin-codemirror6" + }, + { + "path": "../plugin-sql-editor" } ], "include": [ @@ -46,7 +50,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-sql-editor-screen/package.json b/webapp/packages/plugin-sql-editor-screen/package.json index 65defddded..fe26262bf8 100644 --- a/webapp/packages/plugin-sql-editor-screen/package.json +++ b/webapp/packages/plugin-sql-editor-screen/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-sql-editor-screen", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,34 +11,41 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-sql-editor": "~0.1.0", - "@cloudbeaver/plugin-sql-editor-navigation-tab": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-data-context": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-routing": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x" + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-routing": "workspace:*", + "@cloudbeaver/core-settings": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-sql-editor": "workspace:*", + "@cloudbeaver/plugin-sql-editor-navigation-tab": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, - "devDependencies": {} + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "typescript": "^5" + } } diff --git a/webapp/packages/plugin-sql-editor-screen/src/LocaleService.ts b/webapp/packages/plugin-sql-editor-screen/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-sql-editor-screen/src/LocaleService.ts +++ b/webapp/packages/plugin-sql-editor-screen/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-sql-editor-screen/src/PluginBootstrap.ts b/webapp/packages/plugin-sql-editor-screen/src/PluginBootstrap.ts index 5370cdce3f..8fcc28c4dc 100644 --- a/webapp/packages/plugin-sql-editor-screen/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-screen/src/PluginBootstrap.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,21 +9,22 @@ import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { MENU_TAB } from '@cloudbeaver/core-ui'; -import { - ACTION_OPEN_IN_TAB, - ActionService, - DATA_CONTEXT_MENU, - IAction, - KEY_BINDING_OPEN_IN_TAB, - KeyBindingService, - MenuService, -} from '@cloudbeaver/core-view'; -import { DATA_CONTEXT_SQL_EDITOR_STATE, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor'; +import { ACTION_OPEN_IN_TAB, ActionService, type IAction, KEY_BINDING_OPEN_IN_TAB, KeyBindingService, MenuService } from '@cloudbeaver/core-view'; +import { DATA_CONTEXT_SQL_EDITOR_STATE, ESqlDataSourceFeatures, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor'; import { DATA_CONTEXT_SQL_EDITOR_TAB } from '@cloudbeaver/plugin-sql-editor-navigation-tab'; -import { SqlEditorScreenService } from './Screen/SqlEditorScreenService'; +import { SqlEditorScreenService } from './Screen/SqlEditorScreenService.js'; +import { SqlEditorScreenSettingsService } from './SqlEditorScreenSettingsService.js'; -@injectable() +@injectable(() => [ + ActionService, + KeyBindingService, + SqlEditorScreenService, + NotificationService, + MenuService, + SqlDataSourceService, + SqlEditorScreenSettingsService, +]) export class PluginBootstrap extends Bootstrap { constructor( private readonly actionService: ActionService, @@ -32,20 +33,25 @@ export class PluginBootstrap extends Bootstrap { private readonly notificationService: NotificationService, private readonly menuService: MenuService, private readonly sqlDataSourceService: SqlDataSourceService, + private readonly sqlEditorScreenSettingsService: SqlEditorScreenSettingsService, ) { super(); } - register(): void | Promise { + override register(): void { this.actionService.addHandler({ id: 'sql-editor-screen', - isActionApplicable: (contexts, action) => action === ACTION_OPEN_IN_TAB && contexts.has(DATA_CONTEXT_SQL_EDITOR_STATE), - isDisabled: (context, action) => { - const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); + actions: [ACTION_OPEN_IN_TAB], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], + isActionApplicable: context => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + + const dataSource = this.sqlDataSourceService.get(state.editorId); - if (!state) { - return false; - } + return !!dataSource?.hasFeature(ESqlDataSourceFeatures.script) && this.sqlEditorScreenSettingsService.enabled; + }, + isDisabled: context => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(state.editorId); return dataSource?.executionContext === undefined; @@ -56,21 +62,27 @@ export class PluginBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor', binding: KEY_BINDING_OPEN_IN_TAB, - isBindingApplicable: (contexts, action) => action === ACTION_OPEN_IN_TAB, + actions: [ACTION_OPEN_IN_TAB], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], + isBindingApplicable: context => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + + const dataSource = this.sqlDataSourceService.get(state.editorId); + + return !!dataSource?.hasFeature(ESqlDataSourceFeatures.script) && this.sqlEditorScreenSettingsService.enabled; + }, handler: this.openTab.bind(this), }); this.menuService.addCreator({ - isApplicable: context => - context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE) !== undefined && - context.has(DATA_CONTEXT_SQL_EDITOR_TAB) && - context.get(DATA_CONTEXT_MENU) === MENU_TAB, + menus: [MENU_TAB], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE, DATA_CONTEXT_SQL_EDITOR_TAB], getItems: (context, items) => [ACTION_OPEN_IN_TAB, ...items], }); } private openTab(contexts: IDataContextProvider, action: IAction) { - const context = contexts.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const context = contexts.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(context.editorId); if (!dataSource?.executionContext) { @@ -87,6 +99,4 @@ export class PluginBootstrap extends Bootstrap { window.open(url, '_blank')?.focus(); } - - async load(): Promise {} } diff --git a/webapp/packages/plugin-sql-editor-screen/src/Screen/ISqlEditorScreenParams.ts b/webapp/packages/plugin-sql-editor-screen/src/Screen/ISqlEditorScreenParams.ts index 8c77227f3a..003bb96767 100644 --- a/webapp/packages/plugin-sql-editor-screen/src/Screen/ISqlEditorScreenParams.ts +++ b/webapp/packages/plugin-sql-editor-screen/src/Screen/ISqlEditorScreenParams.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreen.tsx b/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreen.tsx index a6f343efd5..00b7adaec2 100644 --- a/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreen.tsx +++ b/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreen.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -12,15 +12,15 @@ import { Loader, TextPlaceholder, useObservableRef, useResource, useTranslate } import { ConnectionExecutionContextResource, ConnectionExecutionContextService, - IConnectionExecutionContextInfo, + type IConnectionExecutionContextInfo, } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import type { ScreenComponent } from '@cloudbeaver/core-routing'; import { uuid } from '@cloudbeaver/core-utils'; -import { ISqlEditorTabState, MemorySqlDataSource, SqlDataSourceService, SqlEditor, SqlEditorService } from '@cloudbeaver/plugin-sql-editor'; +import { type ISqlEditorTabState, MemorySqlDataSource, SqlDataSourceService, SqlEditor, SqlEditorService } from '@cloudbeaver/plugin-sql-editor'; -import type { ISqlEditorScreenParams } from './ISqlEditorScreenParams'; +import type { ISqlEditorScreenParams } from './ISqlEditorScreenParams.js'; export const SqlEditorScreen: ScreenComponent = observer(function SqlEditorScreen({ contextId }) { const translate = useTranslate(); diff --git a/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreenBootstrap.ts b/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreenBootstrap.ts index 910e650d4c..319768ecfe 100644 --- a/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreenBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreenBootstrap.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,17 +8,18 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ScreenService } from '@cloudbeaver/core-routing'; -import { SqlEditorScreenService } from './SqlEditorScreenService'; +import { SqlEditorScreenService } from './SqlEditorScreenService.js'; -@injectable() +@injectable(() => [ScreenService, SqlEditorScreenService]) export class SqlEditorScreenBootstrap extends Bootstrap { - constructor(private readonly screenService: ScreenService, private readonly sqlEditorScreenService: SqlEditorScreenService) { + constructor( + private readonly screenService: ScreenService, + private readonly sqlEditorScreenService: SqlEditorScreenService, + ) { super(); } - register(): void { + override register(): void { this.screenService.create(this.sqlEditorScreenService.screen); } - - load(): void | Promise {} } diff --git a/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreenService.ts b/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreenService.ts index 97094b706a..8fda824de6 100644 --- a/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreenService.ts +++ b/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreenService.ts @@ -1,24 +1,31 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { injectable } from '@cloudbeaver/core-di'; -import { IScreen, ScreenService } from '@cloudbeaver/core-routing'; +import { type IScreen, ScreenService } from '@cloudbeaver/core-routing'; -import type { ISqlEditorScreenParams } from './ISqlEditorScreenParams'; -import { SqlEditorScreen } from './SqlEditorScreen'; +import type { ISqlEditorScreenParams } from './ISqlEditorScreenParams.js'; +import { SqlEditorScreen } from './SqlEditorScreen.js'; +import { SqlEditorScreenSettingsService } from '../SqlEditorScreenSettingsService.js'; -@injectable() +@injectable(() => [ScreenService, SqlEditorScreenSettingsService]) export class SqlEditorScreenService { readonly screen: IScreen; - constructor(private readonly screenService: ScreenService) { + constructor( + private readonly screenService: ScreenService, + sqlEditorScreenSettingsService: SqlEditorScreenSettingsService, + ) { this.screen = { name: 'sql-editor', routes: [{ name: 'sql-editor', path: '/sql-editor/:contextId' }], component: SqlEditorScreen, + canDeActivate() { + return sqlEditorScreenSettingsService.enabled; + }, }; } diff --git a/webapp/packages/plugin-sql-editor-screen/src/SqlEditorScreenSettingsService.ts b/webapp/packages/plugin-sql-editor-screen/src/SqlEditorScreenSettingsService.ts new file mode 100644 index 0000000000..5dd1eb78af --- /dev/null +++ b/webapp/packages/plugin-sql-editor-screen/src/SqlEditorScreenSettingsService.ts @@ -0,0 +1,43 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { injectable } from '@cloudbeaver/core-di'; +import { HIGHEST_SETTINGS_LAYER } from '@cloudbeaver/core-root'; +import { createSettingsOverrideResolver, SettingsProvider, SettingsProviderService, SettingsResolverService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; + +const defaultSettings = schema.object({ + 'plugin.sql-editor-screen.enabled': schemaExtra.stringedBoolean().default(true), +}); + +type SqlEditorScreenSettingsSchema = typeof defaultSettings; +export type SqlEditorSettings = schema.infer; + +@injectable(() => [SettingsProviderService, SettingsResolverService]) +export class SqlEditorScreenSettingsService { + get enabled(): boolean { + return this.settings.getValue('plugin.sql-editor-screen.enabled'); + } + + readonly settings: SettingsProvider; + + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsResolverService: SettingsResolverService, + ) { + this.settings = this.settingsProviderService.createSettings(defaultSettings); + this.settingsResolverService.addResolver( + HIGHEST_SETTINGS_LAYER, + createSettingsOverrideResolver(this.settingsProviderService.settingsResolver, { + 'plugin.sql-editor-screen.enabled': { + key: 'permission.sql.script.execution', + filter: value => !value, + }, + }), + ); + } +} diff --git a/webapp/packages/plugin-sql-editor-screen/src/index.ts b/webapp/packages/plugin-sql-editor-screen/src/index.ts index ee6c8b0bda..a219ab52a7 100644 --- a/webapp/packages/plugin-sql-editor-screen/src/index.ts +++ b/webapp/packages/plugin-sql-editor-screen/src/index.ts @@ -1,3 +1,12 @@ -import { sqlEditorPagePluginManifest } from './manifest'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 './module.js'; +import { sqlEditorPagePluginManifest } from './manifest.js'; export default sqlEditorPagePluginManifest; diff --git a/webapp/packages/plugin-sql-editor-screen/src/locales/fr.ts b/webapp/packages/plugin-sql-editor-screen/src/locales/fr.ts new file mode 100644 index 0000000000..e97fdcfd7f --- /dev/null +++ b/webapp/packages/plugin-sql-editor-screen/src/locales/fr.ts @@ -0,0 +1,5 @@ +export default [ + ['sql_editor_screen_no_context_title', "Erreur lors de l'ouverture dans un nouvel onglet"], + ['sql_editor_screen_no_context_message', "Veuillez sélectionner une connexion pour l'éditeur SQL actuel"], + ['sql_editor_screen_context_not_found', 'Contexte non trouvé'], +]; diff --git a/webapp/packages/plugin-sql-editor-screen/src/locales/vi.ts b/webapp/packages/plugin-sql-editor-screen/src/locales/vi.ts new file mode 100644 index 0000000000..ab76ff5afd --- /dev/null +++ b/webapp/packages/plugin-sql-editor-screen/src/locales/vi.ts @@ -0,0 +1,5 @@ +export default [ + ['sql_editor_screen_no_context_title', 'Lỗi khi mở trong tab mới'], + ['sql_editor_screen_no_context_message', 'Vui lòng chọn kết nối cho trình soạn thảo SQL hiện tại'], + ['sql_editor_screen_context_not_found', 'Không tìm thấy ngữ cảnh'], +]; diff --git a/webapp/packages/plugin-sql-editor-screen/src/manifest.ts b/webapp/packages/plugin-sql-editor-screen/src/manifest.ts index 99aa9385ec..e2079727ef 100644 --- a/webapp/packages/plugin-sql-editor-screen/src/manifest.ts +++ b/webapp/packages/plugin-sql-editor-screen/src/manifest.ts @@ -1,21 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { PluginManifest } from '@cloudbeaver/core-di'; -import { LocaleService } from './LocaleService'; -import { PluginBootstrap } from './PluginBootstrap'; -import { SqlEditorScreenBootstrap } from './Screen/SqlEditorScreenBootstrap'; -import { SqlEditorScreenService } from './Screen/SqlEditorScreenService'; - export const sqlEditorPagePluginManifest: PluginManifest = { info: { name: 'Sql Editor Page plugin', }, - - providers: [PluginBootstrap, LocaleService, SqlEditorScreenBootstrap, SqlEditorScreenService], }; diff --git a/webapp/packages/plugin-sql-editor-screen/src/module.ts b/webapp/packages/plugin-sql-editor-screen/src/module.ts new file mode 100644 index 0000000000..46165c74e6 --- /dev/null +++ b/webapp/packages/plugin-sql-editor-screen/src/module.ts @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { PluginBootstrap } from './PluginBootstrap.js'; +import { SqlEditorScreenSettingsService } from './SqlEditorScreenSettingsService.js'; +import { SqlEditorScreenBootstrap } from './Screen/SqlEditorScreenBootstrap.js'; +import { SqlEditorScreenService } from './Screen/SqlEditorScreenService.js'; +import { LocaleService } from './LocaleService.js'; + +ModuleRegistry.add({ + name: '@cloudbeaver/plugin-sql-editor-screen', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Dependency, proxy(SqlEditorScreenSettingsService)) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(Bootstrap, PluginBootstrap) + .addSingleton(Bootstrap, SqlEditorScreenBootstrap) + .addSingleton(SqlEditorScreenSettingsService) + .addSingleton(SqlEditorScreenService); + }, +}); diff --git a/webapp/packages/plugin-sql-editor-screen/tsconfig.json b/webapp/packages/plugin-sql-editor-screen/tsconfig.json index 652506e14d..5a13c9795f 100644 --- a/webapp/packages/plugin-sql-editor-screen/tsconfig.json +++ b/webapp/packages/plugin-sql-editor-screen/tsconfig.json @@ -1,49 +1,59 @@ { - "extends": "../../tsconfig.base.json", + "extends": "@cloudbeaver/tsconfig/tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true }, "references": [ { - "path": "../plugin-sql-editor/tsconfig.json" + "path": "../core-blocks" }, { - "path": "../plugin-sql-editor-navigation-tab/tsconfig.json" + "path": "../core-cli" }, { - "path": "../core-blocks/tsconfig.json" + "path": "../core-connections" }, { - "path": "../core-connections/tsconfig.json" + "path": "../core-data-context" }, { - "path": "../core-data-context/tsconfig.json" + "path": "../core-di" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-events" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-localization" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-resource" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-root" }, { - "path": "../core-routing/tsconfig.json" + "path": "../core-routing" }, { - "path": "../core-ui/tsconfig.json" + "path": "../core-settings" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-ui" }, { - "path": "../core-view/tsconfig.json" + "path": "../core-utils" + }, + { + "path": "../core-view" + }, + { + "path": "../plugin-sql-editor" + }, + { + "path": "../plugin-sql-editor-navigation-tab" } ], "include": [ @@ -55,7 +65,6 @@ ], "exclude": [ "**/node_modules", - "lib/**/*", - "dist/**/*" + "lib/**/*" ] } diff --git a/webapp/packages/plugin-sql-editor/package.json b/webapp/packages/plugin-sql-editor/package.json index 519d7e1122..723427b608 100644 --- a/webapp/packages/plugin-sql-editor/package.json +++ b/webapp/packages/plugin-sql-editor/package.json @@ -1,6 +1,9 @@ { "name": "@cloudbeaver/plugin-sql-editor", + "type": "module", "sideEffects": [ + "./lib/module.js", + "./lib/index.js", "src/**/*.css", "src/**/*.scss", "public/**/*" @@ -8,47 +11,53 @@ "version": "0.1.0", "description": "", "license": "Apache-2.0", - "main": "dist/index.js", + "exports": { + ".": "./lib/index.js" + }, "scripts": { - "build": "core-cli-build --mode=production --config ../core-cli/configs/webpack.plugin.config.js", + "build": "tsc -b", + "clean": "rimraf --glob lib", "lint": "eslint ./src/ --ext .ts,.tsx", - "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", - "pretest": "tsc -b", - "test": "core-cli-test", - "validate-dependencies": "core-cli-validate-dependencies", - "update-ts-references": "rimraf --glob dist/* && typescript-resolve-references" + "test": "dbeaver-test", + "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/plugin-codemirror6": "~0.1.0", - "@cloudbeaver/plugin-data-viewer": "~0.1.0", - "@cloudbeaver/plugin-navigation-tabs": "~0.1.0", - "@cloudbeaver/core-blocks": "~0.1.0", - "@cloudbeaver/core-connections": "~0.1.0", - "@cloudbeaver/core-data-context": "~0.1.0", - "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-dialogs": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", - "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", - "@cloudbeaver/core-ui": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0", - "@cloudbeaver/core-view": "~0.1.0" - }, - "peerDependencies": { - "react": "~18.x.x", - "mobx": "~6.x.x", - "mobx-react-lite": "~3.x.x", - "reshadow": "~0.x.x", - "@testing-library/jest-dom": "~6.x.x" + "@cloudbeaver/core-authentication": "workspace:*", + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-browser": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-navigation-tree": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-settings": "workspace:*", + "@cloudbeaver/core-storage": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/core-view": "workspace:*", + "@cloudbeaver/plugin-codemirror6": "workspace:*", + "@cloudbeaver/plugin-data-viewer": "workspace:*", + "@cloudbeaver/plugin-navigation-tabs": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" }, "devDependencies": { - "@cloudbeaver/tests-runner": "~0.1.0" + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@dbeaver/cli": "workspace:*", + "@dbeaver/react-tests": "workspace:^", + "@types/react": "^19", + "typescript": "^5", + "typescript-plugin-css-modules": "^5", + "vitest": "^3" } } diff --git a/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs.svg b/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs.svg index bc9e4420c2..f188563b19 100644 --- a/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs.svg +++ b/webapp/packages/plugin-sql-editor/public/icons/sql_output_logs.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor/src/ACTION_TAB_CLOSE_SQL_RESULT_GROUP.ts b/webapp/packages/plugin-sql-editor/src/ACTION_TAB_CLOSE_SQL_RESULT_GROUP.ts new file mode 100644 index 0000000000..6f50822b05 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/ACTION_TAB_CLOSE_SQL_RESULT_GROUP.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_TAB_CLOSE_SQL_RESULT_GROUP = createAction('tab-close-sql-result-group', { + label: 'plugin_sql_editor_action_close_group', + tooltip: 'plugin_sql_editor_action_close_group', +}); diff --git a/webapp/packages/plugin-sql-editor/src/DATA_CONTEXT_SQL_EDITOR_STATE.ts b/webapp/packages/plugin-sql-editor/src/DATA_CONTEXT_SQL_EDITOR_STATE.ts index 7391c13884..306c7a1772 100644 --- a/webapp/packages/plugin-sql-editor/src/DATA_CONTEXT_SQL_EDITOR_STATE.ts +++ b/webapp/packages/plugin-sql-editor/src/DATA_CONTEXT_SQL_EDITOR_STATE.ts @@ -1,12 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { createDataContext } from '@cloudbeaver/core-data-context'; -import type { ISqlEditorTabState } from './ISqlEditorTabState'; +import type { ISqlEditorTabState } from './ISqlEditorTabState.js'; export const DATA_CONTEXT_SQL_EDITOR_STATE = createDataContext('sql-editor-state'); diff --git a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts index a117d81396..0e0affa24b 100644 --- a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts +++ b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts @@ -1,67 +1,80 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { IOutputLogType } from './SqlResultTabs/OutputLogs/IOutputLogTypes'; +import { schema } from '@cloudbeaver/core-utils'; -export interface IResultTab { - tabId: string; - // when query return several results they all have one groupId - // new group id generates every time you execute query in new tab - groupId: string; - indexInResultSet: number; -} +import { OUTPUT_LOG_TYPES } from './SqlResultTabs/OutputLogs/IOutputLogTypes.js'; -export interface IStatisticsTab { - tabId: string; - order: number; -} +export const RESULT_TAB_SCHEMA = schema.object({ + tabId: schema.string(), + groupId: schema.string(), + indexInResultSet: schema.number(), + presentationId: schema.string(), + valuePresentationId: schema.nullable(schema.string()), +}); -export interface IResultGroup { - groupId: string; - modelId: string; - order: number; - nameOrder: number; - query: string; -} +export type IResultTab = schema.infer; -export interface ISqlEditorResultTab { - id: string; - order: number; - name: string; - icon: string; -} +export const STATISTIC_TAB_SCHEMA = schema.object({ + tabId: schema.string(), + order: schema.number(), +}); -export interface IExecutionPlanTab { - tabId: string; - order: number; - query: string; - options?: Record; -} +export type IStatisticsTab = schema.infer; -export interface IOutputLogsTab extends ISqlEditorResultTab { - selectedLogTypes: IOutputLogType[]; -} +export const RESULT_GROUP_SCHEMA = schema.object({ + groupId: schema.string(), + modelId: schema.string(), + order: schema.number(), + nameOrder: schema.number(), + query: schema.string(), +}); -export interface ISqlEditorTabState { - editorId: string; - datasourceKey: string; +export type IResultGroup = schema.infer; - source?: string; - order: number; +export const SQL_EDITOR_RESULT_TAB_SCHEMA = schema.object({ + id: schema.string(), + order: schema.number(), + name: schema.string(), + icon: schema.string(), +}); - currentTabId?: string; - tabs: ISqlEditorResultTab[]; - resultGroups: IResultGroup[]; - resultTabs: IResultTab[]; - statisticsTabs: IStatisticsTab[]; - executionPlanTabs: IExecutionPlanTab[]; - outputLogsTab?: IOutputLogsTab; +export type ISqlEditorResultTab = schema.infer; - // mode - currentModeId?: string; - modeState: Array<[string, any]>; -} +export const EXECUTION_PLAN_TAB_SCHEMA = schema.object({ + tabId: schema.string(), + order: schema.number(), + query: schema.string(), + options: schema.record(schema.string(), schema.any()).optional(), +}); + +export type IExecutionPlanTab = schema.infer; + +const OUTPUT_LOGS_TAB_SCHEMA = SQL_EDITOR_RESULT_TAB_SCHEMA.extend({ + selectedLogTypes: schema.array(schema.enum(OUTPUT_LOG_TYPES)), +}); + +export type IOutputLogsTab = schema.infer; + +export const SQL_EDITOR_TAB_STATE_SCHEMA = schema.object({ + editorId: schema.string(), + datasourceKey: schema.string(), + source: schema.string().optional(), + order: schema.number(), + currentTabId: schema.string().optional(), + tabs: schema.array(SQL_EDITOR_RESULT_TAB_SCHEMA), + resultGroups: schema.array(RESULT_GROUP_SCHEMA), + resultTabs: schema.array(RESULT_TAB_SCHEMA), + statisticsTabs: schema.array(STATISTIC_TAB_SCHEMA), + executionPlanTabs: schema.array(EXECUTION_PLAN_TAB_SCHEMA), + outputLogsTab: OUTPUT_LOGS_TAB_SCHEMA.optional(), + currentModeId: schema.string().optional(), + modeState: schema.array(schema.tuple([schema.string(), schema.any()])), + metadata: schema.record(schema.string(), schema.any()).optional(), +}); + +export type ISqlEditorTabState = schema.infer; diff --git a/webapp/packages/plugin-sql-editor/src/LocaleService.ts b/webapp/packages/plugin-sql-editor/src/LocaleService.ts index e3649a06b4..6d1a499028 100644 --- a/webapp/packages/plugin-sql-editor/src/LocaleService.ts +++ b/webapp/packages/plugin-sql-editor/src/LocaleService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,28 +8,30 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -@injectable() +@injectable(() => [LocalizationService]) export class LocaleService extends Bootstrap { constructor(private readonly localizationService: LocalizationService) { super(); } - register(): void | Promise { + override register(): void { 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; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + case 'vi': + return (await import('./locales/vi.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts index 7b8540f542..8db3a7f483 100644 --- a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts @@ -1,61 +1,277 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { IDataContextProvider } from '@cloudbeaver/core-data-context'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { ACTION_REDO, ACTION_UNDO, ActionService, IAction, KEY_BINDING_REDO, KEY_BINDING_UNDO, KeyBindingService } from '@cloudbeaver/core-view'; - -import { ACTION_SQL_EDITOR_EXECUTE } from './actions/ACTION_SQL_EDITOR_EXECUTE'; -import { ACTION_SQL_EDITOR_EXECUTE_NEW } from './actions/ACTION_SQL_EDITOR_EXECUTE_NEW'; -import { ACTION_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/ACTION_SQL_EDITOR_EXECUTE_SCRIPT'; -import { ACTION_SQL_EDITOR_FORMAT } from './actions/ACTION_SQL_EDITOR_FORMAT'; -import { ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN'; -import { ACTION_SQL_EDITOR_SHOW_OUTPUT } from './actions/ACTION_SQL_EDITOR_SHOW_OUTPUT'; -import { KEY_BINDING_SQL_EDITOR_EXECUTE } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE'; -import { KEY_BINDING_SQL_EDITOR_EXECUTE_NEW } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE_NEW'; -import { KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT'; -import { KEY_BINDING_SQL_EDITOR_FORMAT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_FORMAT'; -import { KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN'; -import { ESqlDataSourceFeatures } from './SqlDataSource/ESqlDataSourceFeatures'; -import { DATA_CONTEXT_SQL_EDITOR_DATA } from './SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA'; - -@injectable() +import { WindowEventsService } from '@cloudbeaver/core-root'; +import { download, getTextFileReadingProcess, throttle, withTimestamp } from '@cloudbeaver/core-utils'; +import { + ACTION_DOWNLOAD, + ACTION_REDO, + ACTION_SAVE, + ACTION_UNDO, + ACTION_UPLOAD, + ActionService, + type IAction, + KEY_BINDING_REDO, + KEY_BINDING_SAVE, + KEY_BINDING_UNDO, + KeyBindingService, + menuExtractItems, + MenuService, +} from '@cloudbeaver/core-view'; +import { ConnectionInfoResource, createConnectionParam, type Connection } from '@cloudbeaver/core-connections'; +import { promptForFiles } from '@cloudbeaver/core-browser'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +import { ACTION_SQL_EDITOR_EXECUTE } from './actions/ACTION_SQL_EDITOR_EXECUTE.js'; +import { ACTION_SQL_EDITOR_EXECUTE_NEW } from './actions/ACTION_SQL_EDITOR_EXECUTE_NEW.js'; +import { ACTION_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/ACTION_SQL_EDITOR_EXECUTE_SCRIPT.js'; +import { ACTION_SQL_EDITOR_FORMAT } from './actions/ACTION_SQL_EDITOR_FORMAT.js'; +import { ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN.js'; +import { KEY_BINDING_SQL_EDITOR_EXECUTE } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE.js'; +import { KEY_BINDING_SQL_EDITOR_EXECUTE_NEW } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE_NEW.js'; +import { KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT.js'; +import { KEY_BINDING_SQL_EDITOR_FORMAT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_FORMAT.js'; +import { KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN.js'; +import { DATA_CONTEXT_SQL_EDITOR_STATE } from './DATA_CONTEXT_SQL_EDITOR_STATE.js'; +import { ESqlDataSourceFeatures } from './SqlDataSource/ESqlDataSourceFeatures.js'; +import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService.js'; +import { DATA_CONTEXT_SQL_EDITOR_DATA } from './SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA.js'; +import { SQL_EDITOR_TOOLS_MENU } from './SqlEditor/SQL_EDITOR_TOOLS_MENU.js'; +import { SQL_EDITOR_TOOLS_MORE_MENU } from './SqlEditor/SQL_EDITOR_TOOLS_MORE_MENU.js'; +import { SQL_EDITOR_ACTIONS_MENU } from './SqlEditor/SQL_EDITOR_ACTIONS_MENU.js'; +import { getSqlEditorName } from './getSqlEditorName.js'; +import type { ISqlEditorTabState } from './ISqlEditorTabState.js'; +import { SqlEditorSettingsService } from './SqlEditorSettingsService.js'; + +const SYNC_DELAY = 5 * 60 * 1000; + +const ScriptImportDialog = importLazyComponent(() => import('./SqlEditor/ScriptImportDialog.js').then(m => m.ScriptImportDialog)); +const EXECUTIONS_ACTIONS = [ + ACTION_SQL_EDITOR_EXECUTE, + ACTION_SQL_EDITOR_EXECUTE_NEW, + ACTION_SQL_EDITOR_EXECUTE_SCRIPT, + ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, +]; + +@injectable(() => [ + MenuService, + ActionService, + KeyBindingService, + SqlDataSourceService, + WindowEventsService, + ConnectionInfoResource, + SqlEditorSettingsService, + NotificationService, + CommonDialogService, +]) export class MenuBootstrap extends Bootstrap { - constructor(private readonly actionService: ActionService, private readonly keyBindingService: KeyBindingService) { + constructor( + private readonly menuService: MenuService, + private readonly actionService: ActionService, + private readonly keyBindingService: KeyBindingService, + private readonly sqlDataSourceService: SqlDataSourceService, + private readonly windowEventsService: WindowEventsService, + private readonly connectionInfoResource: ConnectionInfoResource, + private readonly sqlEditorSettingsService: SqlEditorSettingsService, + private readonly notificationService: NotificationService, + private readonly commonDialogService: CommonDialogService, + ) { super(); } - register(): void { + override register(): void { + this.windowEventsService.onFocusChange.addHandler(throttle(this.focusChangeHandler.bind(this), SYNC_DELAY, false)); this.actionService.addHandler({ - id: 'sql-editor-actions', - isActionApplicable: (contexts, action): boolean => { - const sqlEditorData = contexts.tryGet(DATA_CONTEXT_SQL_EDITOR_DATA); + id: 'sql-editor-base-handler', + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], + actions: [ACTION_SAVE], + isActionApplicable: (context, action): boolean => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; - if (!sqlEditorData) { - return false; + const dataSource = this.sqlDataSourceService.get(state.editorId); + + if (action === ACTION_SAVE) { + return dataSource?.isAutoSaveEnabled === false; + } + + return false; + }, + handler: async (context, action) => { + if (action === ACTION_SAVE) { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + const source = this.sqlDataSourceService.get(state.editorId); + + if (!source) { + return; + } + + await source.save(); + } + }, + isDisabled: (context, action) => { + if (action === ACTION_SAVE) { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + const source = this.sqlDataSourceService.get(state.editorId); + + if (!source) { + return true; + } + + return source.isLoading() || source.isSaved || source.isReadonly(); + } + + return false; + }, + getActionInfo: (context, action) => { + if (action === ACTION_SAVE) { + return { + ...action.info, + label: '', + }; + } + + return action.info; + }, + }); + + this.menuService.addCreator({ + menus: [SQL_EDITOR_TOOLS_MENU], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], + getItems: (context, items) => [...items, ACTION_SQL_EDITOR_FORMAT, SQL_EDITOR_TOOLS_MORE_MENU], + orderItems(context, items) { + const extracted = menuExtractItems(items, [SQL_EDITOR_TOOLS_MORE_MENU]); + return [...items, ...extracted]; + }, + }); + this.menuService.addCreator({ + menus: [SQL_EDITOR_TOOLS_MENU], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], + getItems: (context, items) => [...items, ACTION_DOWNLOAD, ACTION_UPLOAD], + }); + + this.actionService.addHandler({ + id: 'sql-editor-actions-more', + actions: [ACTION_DOWNLOAD, ACTION_UPLOAD], + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA, DATA_CONTEXT_SQL_EDITOR_STATE], + isHidden: context => { + const data = context.get(DATA_CONTEXT_SQL_EDITOR_DATA)!; + + return data.activeSegmentMode.activeSegmentMode; + }, + isDisabled: (context, action) => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + + const dataSource = this.sqlDataSourceService.get(state.editorId); + switch (action) { + case ACTION_DOWNLOAD: + return !dataSource?.script; + case ACTION_UPLOAD: + return !!dataSource?.isReadonly(); + } + + return false; + }, + getActionInfo(context, action) { + switch (action) { + case ACTION_DOWNLOAD: + return { + ...action.info, + icon: '/icons/export.svg', + label: 'sql_editor_download_script_tooltip', + tooltip: 'sql_editor_download_script_tooltip', + }; + + case ACTION_UPLOAD: + return { + ...action.info, + icon: '/icons/import.svg', + label: 'sql_editor_upload_script_tooltip', + tooltip: 'sql_editor_upload_script_tooltip', + }; + } + return action.info; + }, + handler: this.sqlEditorActionHandler.bind(this), + }); + this.menuService.addCreator({ + menus: [SQL_EDITOR_TOOLS_MENU], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], + isApplicable: context => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + + const dataSource = this.sqlDataSourceService.get(state.editorId); + + return !!dataSource?.hasFeature(ESqlDataSourceFeatures.script); + }, + getItems: (context, items) => [...items, ACTION_SAVE], + }); + + this.menuService.addCreator({ + menus: [SQL_EDITOR_ACTIONS_MENU], + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA, DATA_CONTEXT_SQL_EDITOR_STATE], + getItems: (context, items) => [ + ...items, + ...EXECUTIONS_ACTIONS, + ], + }); + + this.keyBindingService.addKeyBindingHandler({ + id: 'sql-editor-save', + binding: KEY_BINDING_SAVE, + actions: [ACTION_SAVE], + contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], + handler: async context => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + const source = this.sqlDataSourceService.get(state.editorId); + + if (!source) { + return; } - if (sqlEditorData.readonly && [ACTION_SQL_EDITOR_FORMAT, ACTION_REDO, ACTION_UNDO].includes(action)) { + await source.save(); + }, + }); + + this.actionService.addHandler({ + id: 'sql-editor-actions', + actions: [ + ...EXECUTIONS_ACTIONS, + ACTION_SQL_EDITOR_FORMAT, + ACTION_REDO, + ACTION_UNDO, + ], + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], + isActionApplicable: (contexts, action): boolean => { + const sqlEditorData = contexts.get(DATA_CONTEXT_SQL_EDITOR_DATA)!; + + if (sqlEditorData.readonly && [ACTION_REDO, ACTION_UNDO].includes(action)) { return false; } if ( - !sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) && - [ - ACTION_SQL_EDITOR_EXECUTE, - ACTION_SQL_EDITOR_EXECUTE_NEW, - ACTION_SQL_EDITOR_EXECUTE_SCRIPT, - ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, - ACTION_SQL_EDITOR_SHOW_OUTPUT, - ].includes(action) + !sqlEditorData.isExecutionAllowed && + EXECUTIONS_ACTIONS.includes(action) ) { return false; } + if (action === ACTION_SQL_EDITOR_FORMAT) { + return !!sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.script) && !sqlEditorData.activeSegmentMode.activeSegmentMode; + } + + if (action === ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN) { + return !!sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.query) && + !!sqlEditorData.dialect?.supportsExplainExecutionPlan; + } + // TODO we have to add check for output action ? if ( !sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.query) && @@ -64,24 +280,38 @@ export class MenuBootstrap extends Bootstrap { return false; } - return [ - ACTION_SQL_EDITOR_EXECUTE, - ACTION_SQL_EDITOR_EXECUTE_NEW, - ACTION_SQL_EDITOR_EXECUTE_SCRIPT, - ACTION_SQL_EDITOR_FORMAT, - ACTION_REDO, - ACTION_UNDO, - ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, - ACTION_SQL_EDITOR_SHOW_OUTPUT, - ].includes(action); + return true; + }, + isDisabled: (context, action) => { + const data = context.get(DATA_CONTEXT_SQL_EDITOR_DATA)!; + + if (EXECUTIONS_ACTIONS.includes(action)) { + return data.isDisabled || data.isScriptEmpty; + } + + switch (action) { + case ACTION_SQL_EDITOR_FORMAT: + return data.isDisabled || data.isScriptEmpty || data.readonly; + } + + return false; + }, + getActionInfo: (context, action) => { + if (EXECUTIONS_ACTIONS.includes(action)) { + return { + ...action.info, + label: '', + }; + } + return action.info; }, - isDisabled: (context, action) => !context.has(DATA_CONTEXT_SQL_EDITOR_DATA), handler: this.sqlEditorActionHandler.bind(this), }); this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-execute', binding: KEY_BINDING_SQL_EDITOR_EXECUTE, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_EXECUTE, handler: this.sqlEditorActionHandler.bind(this), }); @@ -89,6 +319,7 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-execute-new', binding: KEY_BINDING_SQL_EDITOR_EXECUTE_NEW, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_EXECUTE_NEW, handler: this.sqlEditorActionHandler.bind(this), }); @@ -96,9 +327,10 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-execute-script', binding: KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => { - const sqlEditorData = contexts.tryGet(DATA_CONTEXT_SQL_EDITOR_DATA); - return action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT && sqlEditorData?.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) === true; + const sqlEditorData = contexts.get(DATA_CONTEXT_SQL_EDITOR_DATA); + return action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT && sqlEditorData?.isExecutionAllowed === true; }, handler: this.sqlEditorActionHandler.bind(this), }); @@ -106,6 +338,7 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-format', binding: KEY_BINDING_SQL_EDITOR_FORMAT, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_FORMAT, handler: this.sqlEditorActionHandler.bind(this), }); @@ -113,6 +346,7 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-redo', binding: KEY_BINDING_REDO, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_REDO, handler: this.sqlEditorActionHandler.bind(this), }); @@ -120,6 +354,7 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-undo', binding: KEY_BINDING_UNDO, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_UNDO, handler: this.sqlEditorActionHandler.bind(this), }); @@ -127,13 +362,14 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-show-execution-plan', binding: KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN, + contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, handler: this.sqlEditorActionHandler.bind(this), }); // this.menuService.addCreator({ // isApplicable: context => ( - // context.tryGet(DATA_CONTEXT_SQL_EDITOR_DATA) !== undefined + // context.get(DATA_CONTEXT_SQL_EDITOR_DATA) !== undefined // && context.get(DATA_CONTEXT_MENU) === MENU_TAB // ), // getItems: (context, items) => [ @@ -144,9 +380,21 @@ export class MenuBootstrap extends Bootstrap { } private sqlEditorActionHandler(context: IDataContextProvider, action: IAction): void { - const data = context.get(DATA_CONTEXT_SQL_EDITOR_DATA); + const data = context.get(DATA_CONTEXT_SQL_EDITOR_DATA)!; switch (action) { + case ACTION_DOWNLOAD: + { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + this.downloadSql(state); + } + break; + case ACTION_UPLOAD: + { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + this.uploadSql(state); + } + break; case ACTION_SQL_EDITOR_EXECUTE: data.executeQuery(); break; @@ -179,5 +427,85 @@ export class MenuBootstrap extends Bootstrap { } } - load(): void | Promise {} + private focusChangeHandler(focused: boolean) { + if (focused) { + const dataSources = this.sqlDataSourceService.dataSources.values(); + + for (const [_, dataSource] of dataSources) { + dataSource.markOutdated(); + } + } + } + + private downloadSql(state: ISqlEditorTabState) { + const dataSource = this.sqlDataSourceService.get(state.editorId); + + if (!dataSource) { + return; + } + + const executionContext = dataSource?.executionContext; + let connection: Connection | undefined; + + if (executionContext) { + connection = this.connectionInfoResource.get(createConnectionParam(executionContext.projectId, executionContext.connectionId)); + } + + const name = getSqlEditorName(state, dataSource, connection); + const script = dataSource.script; + + const blob = new Blob([script], { + type: 'application/sql', + }); + download(blob, `${withTimestamp(name)}.sql`); + } + + private async uploadSql(state: ISqlEditorTabState) { + const dataSource = this.sqlDataSourceService.get(state.editorId); + + if (!dataSource) { + return; + } + const files = await promptForFiles({ accept: '.sql,.txt' }); + const file = files[0]; + + if (!file) { + throw new Error('File is not found'); + } + + const maxSize = this.sqlEditorSettingsService.maxFileSize; + + const size = Math.round(file.size / 1024); // kilobyte + const aboveMaxSize = size > maxSize; + + if (aboveMaxSize) { + this.notificationService.logInfo({ + title: 'sql_editor_upload_script_max_size_title', + message: `Max size: ${maxSize}KB\nFile size: ${size}KB`, + autoClose: false, + }); + + return; + } + + const prevScript = dataSource.script.trim(); + if (prevScript) { + const result = await this.commonDialogService.open(ScriptImportDialog, null); + + if (result === DialogueStateResult.Rejected) { + return; + } + + if (result !== DialogueStateResult.Resolved && result) { + this.downloadSql(state); + } + } + + try { + const script = await getTextFileReadingProcess(file).promise; + dataSource.setScript(script); + } catch (exception: any) { + this.notificationService.logException(exception, 'Uploading script error'); + } + } } diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index 95355bff36..7d20a53e3e 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,22 +8,25 @@ import { makeObservable, observable } from 'mobx'; import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; -import type { IServiceInjector } from '@cloudbeaver/core-di'; +import type { IServiceProvider } from '@cloudbeaver/core-di'; import type { ITask } from '@cloudbeaver/core-executor'; +import { AsyncTaskInfoService } from '@cloudbeaver/core-root'; import { - AsyncTaskInfoService, GraphQLService, ResultDataFormat, - SqlExecuteInfo, - SqlQueryResults, - UpdateResultsDataBatchMutationVariables, + type SqlExecuteInfo, + type SqlQueryResults, + type AsyncUpdateResultsDataBatchMutationVariables, + type AsyncTaskInfo, } from '@cloudbeaver/core-sdk'; +import { uuid } from '@cloudbeaver/core-utils'; import { - DatabaseDataSource, DocumentEditAction, - IDatabaseDataOptions, - IDatabaseResultSet, - IRequestInfo, + type IDatabaseDataOptions, + type IDatabaseResultSet, + type IRequestInfo, + type IResultSetBlobValue, + ResultSetDataSource, ResultSetEditAction, } from '@cloudbeaver/plugin-data-viewer'; @@ -35,24 +38,26 @@ export interface IQueryRequestInfo extends IRequestInfo { query: string; } -export class QueryDataSource extends DatabaseDataSource { +export class QueryDataSource extends ResultSetDataSource { currentTask: ITask | null; - requestInfo: IQueryRequestInfo; + override requestInfo: IQueryRequestInfo; + /** When true, passes isExecuteAnyway to the API for re-execution despite previous errors */ + executeAnyway = false; - get canCancel(): boolean { + override get canCancel(): boolean { return this.currentTask?.cancellable || false; } - get cancelled(): boolean { + override get cancelled(): boolean { return this.currentTask?.cancelled || false; } constructor( - readonly serviceInjector: IServiceInjector, - protected readonly graphQLService: GraphQLService, - protected readonly asyncTaskInfoService: AsyncTaskInfoService, + override readonly serviceProvider: IServiceProvider, + graphQLService: GraphQLService, + asyncTaskInfoService: AsyncTaskInfoService, ) { - super(serviceInjector); + super(serviceProvider, graphQLService, asyncTaskInfoService); this.currentTask = null; this.requestInfo = { @@ -69,26 +74,19 @@ export class QueryDataSource { - if (this.currentTask) { - await this.currentTask.cancel(); - } + override async cancel(): Promise { + await super.cancel(); + await this.currentTask?.cancel(); } async save(prevResults: IDatabaseResultSet[]): Promise { - if (!this.options || !this.executionContext?.context) { + const executionContext = this.executionContext; + + if (!this.options || !executionContext?.context) { return prevResults; } @@ -98,41 +96,73 @@ export class QueryDataSource { + const { taskInfo } = await this.graphQLService.sdk.asyncUpdateResultsDataBatch(updateVariables); + return taskInfo; + }); - this.requestInfo = { - ...this.requestInfo, - requestDuration: response.result.duration, - requestMessage: 'Saved successfully', - source: this.options.query, - }; + this.currentTask = executionContext.run( + async () => { + const info = await this.asyncTaskInfoService.run(task); + const { result } = await this.graphQLService.sdk.getSqlExecuteTaskResults({ taskId: info.id }); + + return result; + }, + () => this.asyncTaskInfoService.cancel(task.id), + () => this.asyncTaskInfoService.remove(task.id), + ); + + const response = await this.currentTask; if (editor) { - const responseResult = this.transformResults(executionContextInfo, response.result.results, 0).find( - newResult => newResult.id === result.id, - ); + const responseResult = this.transformResults(executionContextInfo, response.results, 0).find(newResult => newResult.id === result.id); if (responseResult) { editor.applyUpdate(responseResult); } } + + this.requestInfo = { + ...this.requestInfo, + requestDuration: response.duration, + requestMessage: 'plugin_data_viewer_result_set_save_success', + source: this.options.query, + }; } this.clearError(); } catch (exception: any) { @@ -142,26 +172,19 @@ export class QueryDataSource { + this.executeAnyway = true; + try { + await this.requestData(); + } finally { + this.executeAnyway = false; } - - return this.transformResults(executionContextInfo, response.results, limit); } async request(prevResults: IDatabaseResultSet[]): Promise { @@ -176,34 +199,11 @@ export class QueryDataSource { - const { taskInfo } = await this.graphQLService.sdk.asyncSqlExecuteQuery({ - connectionId: executionContextInfo.connectionId, - contextId: executionContextInfo.id, - query: options.query, - resultId: firstResultId, - filter: { - offset: this.offset, - limit, - constraints: options.constraints, - where: options.whereFilter || undefined, - }, - dataFormat: this.dataFormat, - readLogs: options.readLogs, - }); - - return taskInfo; - }); + const task = this.asyncTaskInfoService.create(() => this.executeQuery(executionContextInfo, options, firstResultId, limit)); this.currentTask = executionContext.run( async () => { @@ -219,15 +219,13 @@ export class QueryDataSource { + const { taskInfo } = await this.graphQLService.sdk.asyncSqlExecuteQuery({ + projectId: executionContextInfo.projectId, + connectionId: executionContextInfo.connectionId, + contextId: executionContextInfo.id, + query: options.query, + resultId: firstResultId, + filter: { + offset: this.offset, + limit, + constraints: options.constraints, + where: options.whereFilter || undefined, + }, + dataFormat: this.dataFormat, + readLogs: options.readLogs, + isExecuteAnyway: this.executeAnyway || undefined, + } as Parameters[0] & { isExecuteAnyway?: boolean }); - for (const result of results) { - if (result.id === null) { - continue; - } - try { - await this.graphQLService.sdk.closeResult({ - connectionId: result.connectionId, - contextId: result.contextId, - resultId: result.id, - }); - } catch (exception: any) { - console.log(`Error closing result (${result.id}):`, exception); - } + return taskInfo; + } + + private innerGetResults( + executionContextInfo: IConnectionExecutionContextInfo, + response: SqlExecuteInfo, + limit: number, + ): IDatabaseResultSet[] | null { + this.requestInfo = { + originalQuery: response.fullQuery || this.options?.query || '', + requestDuration: response.duration || 0, + requestMessage: response.statusMessage || '', + requestFilter: response.filterText || '', + source: this.options?.query || null, + query: this.options?.query || '', + }; + + if (!response.results) { + return null; } + + return this.transformResults(executionContextInfo, response.results, limit); } private transformResults(executionContextInfo: IConnectionExecutionContextInfo, results: SqlQueryResults[], limit: number): IDatabaseResultSet[] { return results.map((result, index) => ({ id: result.resultSet?.id || null, uniqueResultId: `${executionContextInfo.connectionId}_${executionContextInfo.id}_${index}`, + projectId: executionContextInfo.projectId, connectionId: executionContextInfo.connectionId, contextId: executionContextInfo.id, dataFormat: result.dataFormat!, updateRowCount: result.updateRowCount || 0, - loadedFully: (result.resultSet?.rows?.length || 0) < limit, + loadedFully: (result.resultSet?.rowsWithMetaData?.length || 0) < limit, + count: result.resultSet?.rowsWithMetaData?.length || 0, + totalCount: null, // allays returns false // || !result.resultSet?.hasMoreData, data: result.resultSet, })); } - - async dispose(): Promise { - await this.closeResults(this.results); - await this.cancel(); - } } diff --git a/webapp/packages/plugin-sql-editor/src/SQLEditorLoader.ts b/webapp/packages/plugin-sql-editor/src/SQLEditorLoader.ts new file mode 100644 index 0000000000..04387a025b --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SQLEditorLoader.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const SqlEditor = importLazyComponent(() => import('./SqlEditor.js').then(m => m.SqlEditor)); diff --git a/webapp/packages/plugin-sql-editor/src/SQLParser.ts b/webapp/packages/plugin-sql-editor/src/SQLParser.ts index d663bad93c..4bad699980 100644 --- a/webapp/packages/plugin-sql-editor/src/SQLParser.ts +++ b/webapp/packages/plugin-sql-editor/src/SQLParser.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ export interface ISQLScriptLine { export class SQLParser { get scripts(): ISQLScriptSegment[] { - this.update(); return this._scripts; } @@ -39,30 +38,25 @@ export class SQLParser { private _scripts: ISQLScriptSegment[]; private script: string; - private parsedScript: string | null; constructor() { this._scripts = []; this.script = ''; - this.parsedScript = null; - makeObservable(this, { + makeObservable(this, { actualScript: computed, _scripts: observable.ref, script: observable.ref, - parsedScript: observable.ref, getScriptSegment: action, getSegment: action, getQueryAtPos: action, setScript: action, - parse: action, setQueries: action, - update: action, }); } getScriptSegment(): ISQLScriptSegment { - const script = this.parsedScript || ''; + const script = this.script || ''; return { query: script, @@ -80,10 +74,8 @@ export class SQLParser { end = begin; } - this.update(); - return { - query: (this.parsedScript || '').substring(begin, end), + query: (this.script || '').substring(begin, end), begin, end, }; @@ -107,15 +99,9 @@ export class SQLParser { setScript(script: string): void { this.script = script; - this.update(); - } - - parse(script: string): void { - this.parsedScript = script; } setQueries(queries: IQueryInfo[]): this { - this.update(); this._scripts = queries.map(query => ({ query: this.script.substring(query.start, query.end), begin: query.start, @@ -124,10 +110,4 @@ export class SQLParser { return this; } - - private update() { - if (this.parsedScript !== this.script) { - this.parse(this.script); - } - } } diff --git a/webapp/packages/plugin-sql-editor/src/SQL_EDITOR_SETTINGS_GROUP.ts b/webapp/packages/plugin-sql-editor/src/SQL_EDITOR_SETTINGS_GROUP.ts new file mode 100644 index 0000000000..7fb51e245b --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SQL_EDITOR_SETTINGS_GROUP.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; + +export const SQL_EDITOR_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('plugin_sql_editor_sql_editor_settings_group').setOrder(5); diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts index 650449ddd4..5d74f798df 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,31 +8,38 @@ import { action, computed, makeObservable, observable, toJS } from 'mobx'; import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; -import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import { isContainsException, isObjectsEqual, isValuesEqual, staticImplements } from '@cloudbeaver/core-utils'; -import type { IDatabaseDataModel, IDatabaseResultSet } from '@cloudbeaver/plugin-data-viewer'; +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { isContainsException, isValuesEqual, staticImplements } from '@cloudbeaver/core-utils'; +import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; -import type { IDataQueryOptions } from '../QueryDataSource'; -import { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures'; -import type { ISetScriptData, ISqlDataSource, ISqlDataSourceKey } from './ISqlDataSource'; -import type { ISqlDataSourceHistory } from './SqlDataSourceHistory/ISqlDataSourceHistory'; -import { SqlDataSourceHistory } from './SqlDataSourceHistory/SqlDataSourceHistory'; +import type { QueryDataSource } from '../QueryDataSource.js'; +import { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures.js'; +import type { ISetScriptData, ISqlDataSource, ISqlDataSourceKey, ISqlEditorCursor } from './ISqlDataSource.js'; +import type { ISqlDataSourceHistory } from './SqlDataSourceHistory/ISqlDataSourceHistory.js'; +import { SqlDataSourceHistory } from './SqlDataSourceHistory/SqlDataSourceHistory.js'; const SOURCE_HISTORY = 'history'; @staticImplements() -export abstract class BaseSqlDataSource implements ISqlDataSource { +export abstract class BaseSqlDataSource implements ISqlDataSource { static key = 'base'; + abstract get name(): string | null; + message?: string; + abstract get script(): string; abstract get baseScript(): string; - abstract get baseExecutionContext(): IConnectionExecutionContextInfo | undefined; + abstract get executionContext(): IConnectionExecutionContextInfo | undefined; - databaseModels: IDatabaseDataModel[]; - exception?: Error | Error[] | null | undefined; - message?: string; + abstract get baseExecutionContext(): IConnectionExecutionContextInfo | undefined; + databaseModels: IDatabaseDataModel[]; incomingScript: string | undefined; incomingExecutionContext: IConnectionExecutionContextInfo | undefined | null; + exception?: Error | Error[] | null | undefined; + + get cursor(): ISqlEditorCursor { + return this.innerCursorState; + } get isIncomingChanges(): boolean { return this.incomingScript !== undefined || this.incomingExecutionContext !== null; @@ -66,18 +73,19 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { return this.executionContext?.projectId ?? null; } - get features(): ESqlDataSourceFeatures[] { - return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable]; - } - readonly icon: string; readonly history: ISqlDataSourceHistory; readonly onUpdate: ISyncExecutor; readonly onSetScript: ISyncExecutor; - readonly onDatabaseModelUpdate: ISyncExecutor[]>; + readonly onDatabaseModelUpdate: ISyncExecutor[]>; + + protected get features(): ESqlDataSourceFeatures[] { + return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable]; + } protected outdated: boolean; protected editing: boolean; + protected innerCursorState: ISqlEditorCursor; constructor(icon = '/icons/sql_script_m.svg') { this.icon = icon; @@ -88,6 +96,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { this.message = undefined; this.outdated = true; this.editing = true; + this.innerCursorState = { anchor: 0, head: 0 }; this.history = new SqlDataSourceHistory(); this.onUpdate = new SyncExecutor(); this.onSetScript = new SyncExecutor(); @@ -95,16 +104,26 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { this.onDatabaseModelUpdate.setInitialDataGetter(() => this.databaseModels); this.onSetScript.next(this.onUpdate); - this.onSetScript.addHandler(({ script, source }) => { - if (source === SOURCE_HISTORY) { - return; - } - this.history.add(script); - }); + this.onSetScript.addHandler( + action(({ script, source, cursor }) => { + if (source === SOURCE_HISTORY) { + return; + } + this.history.add(script, source, cursor); + }), + ); - this.history.onNavigate.addHandler(value => this.setScript(value, SOURCE_HISTORY)); + this.history.onNavigate.addHandler( + action(({ value, cursor }) => { + this.setScript(value, SOURCE_HISTORY); - makeObservable(this, { + if (cursor) { + this.setCursor(cursor.anchor, cursor.head); + } + }), + ); + + makeObservable(this, { isSaved: computed, isIncomingChanges: computed, isAutoSaveEnabled: computed, @@ -127,24 +146,24 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { outdated: observable.ref, message: observable.ref, editing: observable.ref, + innerCursorState: observable.ref, incomingScript: observable.ref, incomingExecutionContext: observable.ref, }); } - setScript(script: string, source?: string): void { - this.onSetScript.execute({ script, source }); + setScript(script: string, source?: string, cursor?: ISqlEditorCursor): void { + if (cursor) { + this.setInnerCursorState(cursor); + } + this.onSetScript.execute({ script, source, cursor }); } setIncomingScript(script: string): void { if (script !== this.baseScript) { - if (this.script === this.baseScript) { - this.setBaseScript(script); - this.setScript(script); - } else { - this.incomingScript = script; - } + this.incomingScript = script; } else { + this.setBaseScript(script); this.incomingScript = undefined; } } @@ -178,6 +197,10 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { // } } + isOpened(): boolean { + return true; + } + isError(): boolean { return isContainsException(this.exception); } @@ -218,6 +241,14 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { return this.features.includes(feature); } + setCursor(anchor: number, head = anchor): void { + this.setInnerCursorState({ + anchor, + head, + }); + this.onUpdate.execute(); + } + setEditing(state: boolean): void { this.editing = state; } @@ -251,7 +282,13 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { this.markUpdated(); } - load(): Promise | void {} + load(): Promise | void { + this.markUpdated(); + } + + open(): Promise | void { + this.markUpdated(); + } reset(): Promise | void { this.setScript(this.baseScript); @@ -264,4 +301,12 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { protected abstract setBaseScript(script: string): void; protected abstract setBaseExecutionContext(executionContext: IConnectionExecutionContextInfo | undefined): void; + protected setInnerCursorState(cursor: ISqlEditorCursor): void { + const scriptLength = this.script.length; + + this.innerCursorState = Object.freeze({ + anchor: Math.min(cursor.anchor, scriptLength), + head: Math.min(cursor.head, scriptLength), + }); + } } diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts index 8b3494c13f..c52e082ce7 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts index 8d2eb2f4d3..36fd6f0987 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,11 +8,11 @@ import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; import type { ISyncExecutor } from '@cloudbeaver/core-executor'; import type { ILoadableState } from '@cloudbeaver/core-utils'; -import type { IDatabaseDataModel, IDatabaseResultSet } from '@cloudbeaver/plugin-data-viewer'; +import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; -import type { IDataQueryOptions } from '../QueryDataSource'; -import type { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures'; -import type { ISqlDataSourceHistory } from './SqlDataSourceHistory/ISqlDataSourceHistory'; +import type { QueryDataSource } from '../QueryDataSource.js'; +import type { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures.js'; +import type { ISqlDataSourceHistory } from './SqlDataSourceHistory/ISqlDataSourceHistory.js'; export interface ISqlDataSourceKey { readonly key: string; @@ -20,49 +20,68 @@ export interface ISqlDataSourceKey { export interface ISetScriptData { script: string; + cursor?: ISqlEditorCursor; source?: string; } -export interface ISqlDataSource extends ILoadableState { - readonly sourceKey: string; +export interface ISqlEditorCursor { + readonly anchor: number; + readonly head: number; +} + +export interface ISqlDataSource extends ILoadableState { readonly name: string | null; readonly icon?: string; readonly emptyPlaceholder?: string; + readonly message?: string; + + readonly sourceKey: string; + readonly projectId: string | null; + readonly script: string; + readonly cursor: ISqlEditorCursor; readonly incomingScript?: string; - readonly projectId: string | null; - readonly databaseModels: IDatabaseDataModel[]; - readonly executionContext?: IConnectionExecutionContextInfo; - readonly message?: string; - readonly onUpdate: ISyncExecutor; - readonly onSetScript: ISyncExecutor; - readonly onDatabaseModelUpdate: ISyncExecutor[]>; - readonly features: ESqlDataSourceFeatures[]; readonly history: ISqlDataSourceHistory; + + readonly databaseModels: IDatabaseDataModel[]; + readonly executionContext?: IConnectionExecutionContextInfo; + readonly isAutoSaveEnabled: boolean; readonly isIncomingChanges: boolean; readonly isSaved: boolean; readonly isScriptSaved: boolean; readonly isExecutionContextSaved: boolean; + readonly onUpdate: ISyncExecutor; + readonly onSetScript: ISyncExecutor; + readonly onDatabaseModelUpdate: ISyncExecutor[]>; + + isOpened(): boolean; isReadonly(): boolean; isEditing(): boolean; isOutdated(): boolean; + markOutdated(): void; markUpdated(): void; + hasFeature(feature: ESqlDataSourceFeatures): boolean; canRename(name: string | null): boolean; + setName(name: string | null): void; setProject(projectId: string | null): void; - setScript(script: string, source?: string): void; + setScript(script: string, source?: string, cursor?: ISqlEditorCursor): void; + setCursor(anchor: number, head?: number): void; setEditing(state: boolean): void; setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void; setIncomingExecutionContext(executionContext?: IConnectionExecutionContextInfo): void; setIncomingScript(script?: string): void; + applyIncoming(): void; keepCurrent(): void; + save(): Promise | void; load(): Promise | void; + open(): Promise | void; reset(): Promise | void; dispose(): Promise | void; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/ILocalStorageSqlDataSourceState.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/ILocalStorageSqlDataSourceState.ts index 84ff895a82..6bcc81cc4b 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/ILocalStorageSqlDataSourceState.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/ILocalStorageSqlDataSourceState.ts @@ -1,17 +1,18 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; -import type { ISqlDataSourceHistoryState } from '../SqlDataSourceHistory/ISqlDataSourceHistoryState'; +import type { ISqlDataSourceHistoryState } from '../SqlDataSourceHistory/ISqlDataSourceHistoryState.js'; export interface ILocalStorageSqlDataSourceState { script: string; name?: string; executionContext?: IConnectionExecutionContextInfo; history: ISqlDataSourceHistoryState; + showOnlyResults?: boolean; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts index 2c7707eaa5..07a53de844 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,9 +9,10 @@ import { computed, makeObservable, observable } from 'mobx'; import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; -import { BaseSqlDataSource } from '../BaseSqlDataSource'; -import { ESqlDataSourceFeatures } from '../ESqlDataSourceFeatures'; -import type { ILocalStorageSqlDataSourceState } from './ILocalStorageSqlDataSourceState'; +import { BaseSqlDataSource } from '../BaseSqlDataSource.js'; +import { ESqlDataSourceFeatures } from '../ESqlDataSourceFeatures.js'; +import type { ILocalStorageSqlDataSourceState } from './ILocalStorageSqlDataSourceState.js'; +import type { ISqlEditorCursor } from '../ISqlDataSource.js'; export class LocalStorageSqlDataSource extends BaseSqlDataSource { get baseScript(): string { @@ -22,7 +23,7 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource { return this.state.executionContext; } - static key = 'local-storage'; + static override key = 'local-storage'; get name(): string | null { return this.state.name ?? null; @@ -36,14 +37,22 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource { return this.state.executionContext; } - get features(): ESqlDataSourceFeatures[] { + override get features(): ESqlDataSourceFeatures[] { + if (this.state.showOnlyResults) { + return []; + } return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable, ESqlDataSourceFeatures.setName]; } - get isSaved(): boolean { + override get isSaved(): boolean { return true; } + override get projectId(): string | null { + // we will be able to attach any connection from any project + return null; + } + private state!: ILocalStorageSqlDataSourceState; constructor(state: ILocalStorageSqlDataSourceState) { @@ -57,11 +66,11 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource { }); } - isReadonly(): boolean { + override isReadonly(): boolean { return false; } - setName(name: string | null): void { + override setName(name: string | null): void { this.state.name = name ?? undefined; super.setName(name); } @@ -70,12 +79,17 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource { return true; } - setScript(script: string): void { + // TODO: should we move it to the BaseSqlDataSource? + setShowOnlyResults(showOnlyResults: boolean): void { + this.state.showOnlyResults = showOnlyResults; + } + + override setScript(script: string, source?: string, cursor?: ISqlEditorCursor): void { this.state.script = script; - super.setScript(script); + super.setScript(script, source, cursor); } - setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void { + override setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void { this.state.executionContext = executionContext; super.setExecutionContext(executionContext); } diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap.ts index 9e4133cb09..6e02df5646 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,21 +8,24 @@ import { action, makeObservable, observable } from 'mobx'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; +import { StorageService } from '@cloudbeaver/core-storage'; -import { createSqlDataSourceHistoryInitialState } from '../SqlDataSourceHistory/createSqlDataSourceHistoryInitialState'; -import { validateSqlDataSourceHistoryState } from '../SqlDataSourceHistory/validateSqlDataSourceHistoryState'; -import { ISqlDataSourceOptions, SqlDataSourceService } from '../SqlDataSourceService'; -import type { ILocalStorageSqlDataSourceState } from './ILocalStorageSqlDataSourceState'; -import { LocalStorageSqlDataSource } from './LocalStorageSqlDataSource'; +import { createSqlDataSourceHistoryInitialState } from '../SqlDataSourceHistory/createSqlDataSourceHistoryInitialState.js'; +import { validateSqlDataSourceHistoryState } from '../SqlDataSourceHistory/validateSqlDataSourceHistoryState.js'; +import { type ISqlDataSourceOptions, SqlDataSourceService } from '../SqlDataSourceService.js'; +import type { ILocalStorageSqlDataSourceState } from './ILocalStorageSqlDataSourceState.js'; +import { LocalStorageSqlDataSource } from './LocalStorageSqlDataSource.js'; const localStorageKey = 'local-storage-sql-data-source'; -@injectable() +@injectable(() => [SqlDataSourceService, StorageService]) export class LocalStorageSqlDataSourceBootstrap extends Bootstrap { private readonly dataSourceStateState = new Map(); - constructor(private readonly sqlDataSourceService: SqlDataSourceService, localStorageSaveService: LocalStorageSaveService) { + constructor( + private readonly sqlDataSourceService: SqlDataSourceService, + storageService: StorageService, + ) { super(); this.dataSourceStateState = new Map(); @@ -31,7 +34,7 @@ export class LocalStorageSqlDataSourceBootstrap extends Bootstrap { dataSourceStateState: observable.deep, }); - localStorageSaveService.withAutoSave( + storageService.registerSettings( localStorageKey, this.dataSourceStateState, () => new Map(), @@ -62,7 +65,7 @@ export class LocalStorageSqlDataSourceBootstrap extends Bootstrap { ); } - register(): void | Promise { + override register(): void | Promise { this.sqlDataSourceService.register({ key: LocalStorageSqlDataSource.key, getDataSource: (editorId, options) => new LocalStorageSqlDataSource(this.createState(editorId, options)), @@ -70,8 +73,6 @@ export class LocalStorageSqlDataSourceBootstrap extends Bootstrap { }); } - load(): void | Promise {} - private createState(editorId: string, options?: ISqlDataSourceOptions): ILocalStorageSqlDataSourceState { let state = this.dataSourceStateState.get(editorId); @@ -81,6 +82,7 @@ export class LocalStorageSqlDataSourceBootstrap extends Bootstrap { script: options?.script ?? '', executionContext: options?.executionContext, history: createSqlDataSourceHistoryInitialState(options?.script), + ...options?.dataSourceState, }); this.dataSourceStateState.set(editorId, state); diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts index c9b3c24339..d29a9cd6b2 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,8 +9,9 @@ import { computed, makeObservable, observable } from 'mobx'; import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; -import { BaseSqlDataSource } from './BaseSqlDataSource'; -import { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures'; +import { BaseSqlDataSource } from './BaseSqlDataSource.js'; +import { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures.js'; +import type { ISqlEditorCursor } from './ISqlDataSource.js'; export class MemorySqlDataSource extends BaseSqlDataSource { get baseScript(): string { @@ -19,7 +20,7 @@ export class MemorySqlDataSource extends BaseSqlDataSource { get baseExecutionContext(): IConnectionExecutionContextInfo | undefined { return this._executionContext; } - static key = 'memory'; + static override key = 'memory'; get name(): string | null { return this._name; @@ -33,14 +34,19 @@ export class MemorySqlDataSource extends BaseSqlDataSource { return this._executionContext; } - get features(): ESqlDataSourceFeatures[] { + override get features(): ESqlDataSourceFeatures[] { return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable, ESqlDataSourceFeatures.setName]; } - get isSaved(): boolean { + override get isSaved(): boolean { return true; } + override get projectId(): string | null { + // we will be able to attach any connection from any project + return null; + } + private _name: string | null; private _script: string; private _executionContext?: IConnectionExecutionContextInfo; @@ -60,21 +66,21 @@ export class MemorySqlDataSource extends BaseSqlDataSource { }); } - isReadonly(): boolean { + override isReadonly(): boolean { return false; } - setScript(script: string): void { + override setScript(script: string, source?: string, cursor?: ISqlEditorCursor): void { this._script = script; - super.setScript(script); + super.setScript(script, source, cursor); } - setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void { + override setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void { this._executionContext = executionContext; super.setExecutionContext(executionContext); } - setName(name: string | null): void { + override setName(name: string | null): void { this._name = name; super.setName(name); } diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistory.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistory.ts index f33fdbfb84..5c29862a8f 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistory.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistory.ts @@ -1,18 +1,22 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { ISyncExecutor } from '@cloudbeaver/core-executor'; -import type { ISqlDataSourceHistoryState } from './ISqlDataSourceHistoryState'; +import type { ISqlDataSourceHistoryState } from './ISqlDataSourceHistoryState.js'; +import type { ISqlEditorCursor } from '../ISqlDataSource.js'; export interface ISqlDataSourceHistory { readonly state: ISqlDataSourceHistoryState; - readonly onNavigate: ISyncExecutor; - add(value: string, source?: string): void; + readonly onNavigate: ISyncExecutor<{ + value: string; + cursor?: ISqlEditorCursor; + }>; + add(value: string, source?: string, cursor?: ISqlEditorCursor): void; undo(): void; redo(): void; restore(data: ISqlDataSourceHistoryState): void; diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistoryData.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistoryData.ts index e642695892..4ae0c12aa7 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistoryData.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistoryData.ts @@ -1,12 +1,16 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { ISqlEditorCursor } from '../ISqlDataSource.js'; + export interface ISqlDataSourceHistoryData { value: string; + cursor?: ISqlEditorCursor; + timestamp: number; source?: string; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistoryState.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistoryState.ts index ac09be987b..76347f7e85 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistoryState.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistoryState.ts @@ -1,11 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { ISqlDataSourceHistoryData } from './ISqlDataSourceHistoryData'; +import type { ISqlDataSourceHistoryData } from './ISqlDataSourceHistoryData.js'; export interface ISqlDataSourceHistoryState { history: ISqlDataSourceHistoryData[]; diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/SqlDataSourceHistory.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/SqlDataSourceHistory.ts index b57f742d93..435b5a8b21 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/SqlDataSourceHistory.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/SqlDataSourceHistory.ts @@ -1,24 +1,28 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { makeAutoObservable } from 'mobx'; -import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import { createSqlDataSourceHistoryInitialState } from './createSqlDataSourceHistoryInitialState'; -import type { ISqlDataSourceHistory } from './ISqlDataSourceHistory'; -import type { ISqlDataSourceHistoryState } from './ISqlDataSourceHistoryState'; +import { createSqlDataSourceHistoryInitialState } from './createSqlDataSourceHistoryInitialState.js'; +import type { ISqlDataSourceHistory } from './ISqlDataSourceHistory.js'; +import type { ISqlDataSourceHistoryState } from './ISqlDataSourceHistoryState.js'; +import type { ISqlEditorCursor } from '../ISqlDataSource.js'; -const HISTORY_DELAY = 1000; +const HOT_HISTORY_SIZE = 30; +const COMPRESSED_HISTORY_DELAY = 5000; export class SqlDataSourceHistory implements ISqlDataSourceHistory { state: ISqlDataSourceHistoryState; - readonly onNavigate: ISyncExecutor; - private lastAddTime = 0; + readonly onNavigate: ISyncExecutor<{ + value: string; + cursor?: ISqlEditorCursor; + }>; constructor() { this.state = createSqlDataSourceHistoryInitialState(); @@ -29,26 +33,19 @@ export class SqlDataSourceHistory implements ISqlDataSourceHistory { }); } - add(value: string, source?: string): void { + add(value: string, source?: string, cursor?: ISqlEditorCursor): void { // skip history if value is the same as current - if (this.state.history[this.state.historyIndex].value === value) { + if (this.state.history[this.state.historyIndex]!.value === value) { return; } // remove all history after current index if (this.state.historyIndex + 1 < this.state.history.length) { this.state.history.splice(this.state.historyIndex + 1); - this.lastAddTime = 0; } - if (this.lastAddTime + HISTORY_DELAY < Date.now()) { - this.state.history.push({ value, source }); - this.state.historyIndex = this.state.history.length - 1; - this.lastAddTime = Date.now(); - } else { - // update last history item - this.state.history[this.state.history.length - 1] = { value, source }; - } + this.state.historyIndex = this.state.history.push({ value, source, timestamp: Date.now(), cursor }) - 1; + this.compressHistory(); } undo(): void { @@ -56,8 +53,8 @@ export class SqlDataSourceHistory implements ISqlDataSourceHistory { return; } this.state.historyIndex--; - const value = this.state.history[this.state.historyIndex].value; - this.onNavigate.execute(value); + const prevHistoryItem = this.state.history[this.state.historyIndex]!; + this.onNavigate.execute(prevHistoryItem); } redo(): void { @@ -66,8 +63,8 @@ export class SqlDataSourceHistory implements ISqlDataSourceHistory { } this.state.historyIndex++; - const value = this.state.history[this.state.historyIndex].value; - this.onNavigate.execute(value); + const prevHistoryItem = this.state.history[this.state.historyIndex]!; + this.onNavigate.execute(prevHistoryItem); } restore(state: ISqlDataSourceHistoryState): void { @@ -77,4 +74,25 @@ export class SqlDataSourceHistory implements ISqlDataSourceHistory { clear(): void { this.state = createSqlDataSourceHistoryInitialState(); } + + private compressHistory(): void { + if (this.state.history.length > HOT_HISTORY_SIZE) { + for (let i = this.state.history.length - HOT_HISTORY_SIZE; i > 1; i--) { + const prevEntity = this.state.history[i - 1]!; + const entity = this.state.history[i]!; + + if (prevEntity.timestamp === -1) { + break; + } + + if (entity.timestamp - prevEntity.timestamp < COMPRESSED_HISTORY_DELAY) { + this.state.history.splice(i, 1); + } else { + prevEntity.timestamp = -1; + } + } + + this.state.historyIndex = this.state.history.length - 1; + } + } } diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/createSqlDataSourceHistoryInitialState.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/createSqlDataSourceHistoryInitialState.ts index 0ccc1f575c..c67ff547b7 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/createSqlDataSourceHistoryInitialState.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/createSqlDataSourceHistoryInitialState.ts @@ -1,15 +1,15 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { ISqlDataSourceHistoryState } from './ISqlDataSourceHistoryState'; +import type { ISqlDataSourceHistoryState } from './ISqlDataSourceHistoryState.js'; export function createSqlDataSourceHistoryInitialState(value = ''): ISqlDataSourceHistoryState { return { - history: [{ value, source: 'initial' }], + history: [{ value, source: 'initial', timestamp: Date.now(), cursor: { anchor: 0, head: 0 } }], historyIndex: 0, }; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/validateSqlDataSourceHistoryData.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/validateSqlDataSourceHistoryData.ts index 139ba24aac..38e0418422 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/validateSqlDataSourceHistoryData.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/validateSqlDataSourceHistoryData.ts @@ -1,11 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { ISqlDataSourceHistoryData } from './ISqlDataSourceHistoryData'; +import type { ISqlDataSourceHistoryData } from './ISqlDataSourceHistoryData.js'; export function validateSqlDataSourceHistoryData(data: any): data is ISqlDataSourceHistoryData { return ( diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/validateSqlDataSourceHistoryState.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/validateSqlDataSourceHistoryState.ts index b4500192b3..eb0376fdd5 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/validateSqlDataSourceHistoryState.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceHistory/validateSqlDataSourceHistoryState.ts @@ -1,12 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { ISqlDataSourceHistoryState } from './ISqlDataSourceHistoryState'; -import { validateSqlDataSourceHistoryData } from './validateSqlDataSourceHistoryData'; +import type { ISqlDataSourceHistoryState } from './ISqlDataSourceHistoryState.js'; +import { validateSqlDataSourceHistoryData } from './validateSqlDataSourceHistoryData.js'; export function validateSqlDataSourceHistoryState(state: any): state is ISqlDataSourceHistoryState { return ( diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceService.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceService.ts index b1aa9db07c..0d772ce0f4 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/SqlDataSourceService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,30 +9,32 @@ import { action, computed, makeObservable, observable } from 'mobx'; import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import type { ISqlEditorTabState } from '../ISqlEditorTabState'; -import type { ISqlDataSource } from './ISqlDataSource'; -import { MemorySqlDataSource } from './MemorySqlDataSource'; +import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; +import type { ISqlDataSource } from './ISqlDataSource.js'; +import { MemorySqlDataSource } from './MemorySqlDataSource.js'; +import type { QueryDataSource } from '../QueryDataSource.js'; export interface ISqlDataSourceOptions { name?: string; script?: string; executionContext?: IConnectionExecutionContextInfo; + dataSourceState?: Record; } -type ISqlDataSourceFactory = (editorId: string, options?: ISqlDataSourceOptions) => ISqlDataSource; +type ISqlDataSourceFactory = (editorId: string, options?: ISqlDataSourceOptions) => ISqlDataSource; -interface ISqlDataSourceFabric { +interface ISqlDataSourceFabric { key: string; - getDataSource: ISqlDataSourceFactory; - onDestroy?: (dataSource: ISqlDataSource, editorId: string) => Promise | void; - onUnload?: (dataSource: ISqlDataSource, editorId: string) => Promise | void; - canDestroy?: (dataSource: ISqlDataSource, editorId: string) => Promise | boolean; + getDataSource: ISqlDataSourceFactory; + onDestroy?: (dataSource: ISqlDataSource, editorId: string) => Promise | void; + onUnload?: (dataSource: ISqlDataSource, editorId: string) => Promise | void; + canDestroy?: (dataSource: ISqlDataSource, editorId: string) => Promise | boolean; } interface ISqlDataSourceProvider { - provider: ISqlDataSourceFabric; + provider: ISqlDataSourceFabric; dataSource: ISqlDataSource; isActionActive: boolean; } @@ -50,7 +52,7 @@ export class SqlDataSourceService { readonly onCreate: ISyncExecutor<[string, string]>; readonly onUpdate: ISyncExecutor; - private readonly dataSourceProviders: Map; + private readonly dataSourceProviders: Map>; private readonly providers: Map; constructor() { @@ -76,11 +78,11 @@ export class SqlDataSourceService { }); } - get(editorId: string): ISqlDataSource | undefined { - return this.providers.get(editorId)?.dataSource; + get(editorId: string): ISqlDataSource | undefined { + return this.providers.get(editorId)?.dataSource as ISqlDataSource | undefined; } - create(state: ISqlEditorTabState, key: string, options?: ISqlDataSourceOptions): ISqlDataSource { + create(state: ISqlEditorTabState, key: string, options?: ISqlDataSourceOptions): ISqlDataSource { const editorId = state.editorId; const provider = this.dataSourceProviders.get(key); @@ -110,7 +112,7 @@ export class SqlDataSourceService { this.onCreate.execute([editorId, key]); } - return activeProvider.dataSource; + return activeProvider.dataSource as unknown as ISqlDataSource; } async executeAction(editorId: string, action: (dataSource: ISqlDataSource) => Promise | T, notFound: () => void): Promise { @@ -165,7 +167,7 @@ export class SqlDataSourceService { this.providers.delete(editorId); } - register(dataSourceOptions: ISqlDataSourceFabric) { + register(dataSourceOptions: ISqlDataSourceFabric) { if (this.dataSourceProviders.has(dataSourceOptions.key)) { throw new Error(`SQL Data Source with key (${dataSourceOptions.key}) already registered`); } diff --git a/webapp/packages/plugin-sql-editor/src/SqlDialectInfoService.ts b/webapp/packages/plugin-sql-editor/src/SqlDialectInfoService.ts index 61b1690d7e..eff70592b5 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDialectInfoService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDialectInfoService.ts @@ -1,18 +1,21 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ConnectionDialectResource, IConnectionExecutionContextInfo, IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { ConnectionDialectResource, type IConnectionExecutionContextInfo, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import type { SqlDialectInfo } from '@cloudbeaver/core-sdk'; -@injectable() +@injectable(() => [ConnectionDialectResource, NotificationService]) export class SqlDialectInfoService { - constructor(private readonly connectionDialectResource: ConnectionDialectResource, private readonly notificationService: NotificationService) {} + constructor( + private readonly connectionDialectResource: ConnectionDialectResource, + private readonly notificationService: NotificationService, + ) {} async formatScript(context: IConnectionExecutionContextInfo, query: string): Promise { try { diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor.m.css b/webapp/packages/plugin-sql-editor/src/SqlEditor.m.css deleted file mode 100644 index ca5ced83f6..0000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor.m.css +++ /dev/null @@ -1,12 +0,0 @@ -.captureView { - flex: 1; - display: flex; - overflow: auto; - position: relative; -} -.pane { - composes: theme-typography--body2 from global; -} -.pane:first-child { - flex-direction: column; -} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor.module.css b/webapp/packages/plugin-sql-editor/src/SqlEditor.module.css new file mode 100644 index 0000000000..c3669edbc1 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor.module.css @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.captureView { + flex: 1; + display: flex; + overflow: auto; + position: relative; +} +.pane { + composes: theme-typography--body2 from global; +} +.pane:first-child { + flex-direction: column; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx index 94d2ffd11b..e3dd6cc827 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx @@ -1,30 +1,32 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { Loader, Pane, ResizerControls, s, Split, useS, useSplitUserState, useStyles } from '@cloudbeaver/core-blocks'; +import { Loader, Pane, ResizerControls, s, Split, useS, useSplitUserState } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CaptureView } from '@cloudbeaver/core-view'; -import type { ISqlEditorTabState } from './ISqlEditorTabState'; -import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService'; -import style from './SqlEditor.m.css'; -import { SqlEditorLoader } from './SqlEditor/SqlEditorLoader'; -import { SqlEditorOverlay } from './SqlEditorOverlay'; -import { SqlEditorStatusBar } from './SqlEditorStatusBar'; -import { SqlEditorView } from './SqlEditorView'; -import { SqlResultTabs } from './SqlResultTabs/SqlResultTabs'; -import { useDataSource } from './useDataSource'; -interface Props { +import type { ISqlEditorTabState } from './ISqlEditorTabState.js'; +import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService.js'; +import style from './SqlEditor.module.css'; +import { SqlEditorLoader } from './SqlEditor/SqlEditorLoader.js'; +import { SqlEditorOpenOverlay } from './SqlEditorOpenOverlay.js'; +import { SqlEditorOverlay } from './SqlEditorOverlay.js'; +import { SqlEditorStatusBar } from './SqlEditorStatusBar.js'; +import { SqlEditorView } from './SqlEditorView.js'; +import { SqlResultTabs } from './SqlResultTabs/SqlResultTabs.js'; +import { useDataSource } from './useDataSource.js'; + +export interface SqlEditorProps { state: ISqlEditorTabState; } -export const SqlEditor = observer(function SqlEditor({ state }) { +export const SqlEditor = observer(function SqlEditor({ state }) { const styles = useS(style); const sqlEditorView = useService(SqlEditorView); const sqlDataSourceService = useService(SqlDataSourceService); @@ -33,21 +35,24 @@ export const SqlEditor = observer(function SqlEditor({ state }) { useDataSource(dataSource); const splitState = useSplitUserState(`sql-editor-${dataSource?.sourceKey ?? 'default'}`); + const opened = dataSource?.isOpened() || false; + return ( - + - + - + {opened && } + {!opened && } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA.ts index 0a53c9d549..5cc63c3b81 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA.ts @@ -1,12 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { createDataContext } from '@cloudbeaver/core-data-context'; -import type { ISQLEditorData } from './ISQLEditorData'; +import type { ISQLEditorData } from './ISQLEditorData.js'; export const DATA_CONTEXT_SQL_EDITOR_DATA = createDataContext('sql-editor-data'); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts index 20b99daca7..0252f0ebee 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,36 +8,31 @@ import type { ISyncExecutor } from '@cloudbeaver/core-executor'; import type { SqlDialectInfo } from '@cloudbeaver/core-sdk'; -import type { ISqlDataSource } from '../SqlDataSource/ISqlDataSource'; -import type { SQLProposal } from '../SqlEditorService'; -import type { ISQLScriptSegment, SQLParser } from '../SQLParser'; -import type { ISQLEditorMode } from './SQLEditorModeContext'; +import type { ISqlDataSource, ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource.js'; +import type { SQLProposal } from '../SqlEditorService.js'; +import type { ISQLScriptSegment, SQLParser } from '../SQLParser.js'; +import type { ISQLEditorMode } from './SQLEditorModeContext.js'; export interface ISegmentExecutionData { segment: ISQLScriptSegment; type: 'start' | 'end' | 'error'; } -export interface ICursor { - begin: number; - end: number; -} - export interface ISQLEditorData { - readonly cursor: ICursor; - readonly activeSegmentMode: ISQLEditorMode; + readonly cursor: ISqlEditorCursor; + activeSegmentMode: ISQLEditorMode; readonly parser: SQLParser; readonly dialect: SqlDialectInfo | undefined; readonly activeSegment: ISQLScriptSegment | undefined; readonly cursorSegment: ISQLScriptSegment | undefined; readonly readonly: boolean; readonly editing: boolean; - readonly isLineScriptEmpty: boolean; readonly isScriptEmpty: boolean; readonly isDisabled: boolean; readonly isIncomingChanges: boolean; readonly value: string; readonly incomingValue?: string; + readonly isExecutionAllowed: boolean; readonly dataSource: ISqlDataSource | undefined; readonly onExecute: ISyncExecutor; readonly onSegmentExecute: ISyncExecutor; @@ -47,8 +42,8 @@ export interface ISQLEditorData { /** displays if last getHintProposals call ended with limit */ readonly hintsLimitIsMet: boolean; - updateParserScriptsThrottle(): Promise; - setQuery(query: string): void; + updateParserScriptsDebounced(): Promise; + setScript(query: string, source?: string, cursor?: ISqlEditorCursor): void; init(): void; destruct(): void; setCursor(begin: number, end?: number): void; @@ -57,12 +52,14 @@ export interface ISQLEditorData { executeQueryNewTab(): Promise; showExecutionPlan(): Promise; executeScript(): Promise; - switchEditing(): Promise; + switchEditing(): void; getHintProposals(position: number, simple: boolean): Promise; + getResolvedSegment(): Promise; executeQueryAction( segment: ISQLScriptSegment | undefined, - action: (query: ISQLScriptSegment) => Promise, + action: (query: ISQLScriptSegment) => T | Promise, passEmpty?: boolean, passDisabled?: boolean, ): Promise; + setModeId(tabId: string): void; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISqlEditorProps.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISqlEditorProps.ts index 86656fe337..d6fa31b14b 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISqlEditorProps.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISqlEditorProps.ts @@ -1,11 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { ISqlEditorTabState } from '../ISqlEditorTabState'; +import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; export interface ISqlEditorProps { state: ISqlEditorTabState; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.module.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.module.css new file mode 100644 index 0000000000..2014b90b23 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.module.css @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.container { + composes: theme-border-color-background from global; + display: flex; + flex-direction: column; + justify-content: space-between; + overflow: auto; + border-right: solid 1px; +} + +.actions { + display: flex; + flex-direction: column; + align-items: center; + user-select: none; + + &:empty { + width: initial; + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.tsx new file mode 100644 index 0000000000..df5ed9bef4 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.tsx @@ -0,0 +1,48 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { preventFocusHandler, s, useS } from '@cloudbeaver/core-blocks'; +import { useDataContextLink } from '@cloudbeaver/core-data-context'; +import { useMenu } from '@cloudbeaver/core-view'; + +import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; +import { DATA_CONTEXT_SQL_EDITOR_DATA } from './DATA_CONTEXT_SQL_EDITOR_DATA.js'; +import { DATA_CONTEXT_SQL_EDITOR_STATE } from '../DATA_CONTEXT_SQL_EDITOR_STATE.js'; +import type { ISQLEditorData } from './ISQLEditorData.js'; +import style from './SQLEditorActions.module.css'; +import { SqlEditorActionsMenu } from './SqlEditorActionsMenu.js'; +import { SqlEditorTools } from './SqlEditorTools.js'; +import { SQL_EDITOR_ACTIONS_MENU } from './SQL_EDITOR_ACTIONS_MENU.js'; + +interface Props { + data: ISQLEditorData; + state: ISqlEditorTabState; + className?: string; +} + +export const SQLEditorActions = observer(function SQLEditorActions({ data, state, className }) { + const styles = useS(style); + const menu = useMenu({ menu: SQL_EDITOR_ACTIONS_MENU }); + + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_STATE, state, id); + }); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_DATA, data, id); + }); + + return ( +
+
+ +
+ +
+ ); +}); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorModeContext.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorModeContext.ts index e0c9f25c0d..d446a47b9c 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorModeContext.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorModeContext.ts @@ -1,14 +1,14 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { ISyncContextLoader } from '@cloudbeaver/core-executor'; -import type { ISQLScriptSegment } from '../SQLParser'; -import type { ISQLEditorData } from './ISQLEditorData'; +import type { ISQLScriptSegment } from '../SQLParser.js'; +import type { ISQLEditorData } from './ISQLEditorData.js'; export interface ISQLEditorMode { activeSegment: ISQLScriptSegment | undefined; @@ -16,8 +16,11 @@ export interface ISQLEditorMode { } export const SQLEditorModeContext: ISyncContextLoader = function SQLEditorModeContext(context, data) { + const from = Math.min(data.cursor.anchor, data.cursor.head); + const to = Math.max(data.cursor.anchor, data.cursor.head); + return { - activeSegment: data.parser.getSegment(data.cursor.begin, data.cursor.end), + activeSegment: data.parser.getSegment(from, to), activeSegmentMode: false, }; }; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_ACTIONS_MENU.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_ACTIONS_MENU.ts index 26a0e33bd7..6f5206fefa 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_ACTIONS_MENU.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_ACTIONS_MENU.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const SQL_EDITOR_ACTIONS_MENU = createMenu('sql-editor-actions', ''); +export const SQL_EDITOR_ACTIONS_MENU = createMenu('sql-editor-actions', { label: '' }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_ACTIONS_MENU_STYLES.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_ACTIONS_MENU_STYLES.ts deleted file mode 100644 index 1bfbbb5c0c..0000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_ACTIONS_MENU_STYLES.ts +++ /dev/null @@ -1,67 +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 SQL_EDITOR_ACTIONS_MENU_STYLES = css` - menu-bar { - width: 32px; - display: flex; - flex-direction: column; - align-items: center; - } - - menu-bar-item { - composes: theme-ripple from global; - background: none; - padding: 0; - margin: 0; - height: 32px; - width: 32px; - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - flex-shrink: 0; - - &[use|hidden] { - display: none; - } - - & IconOrImage, - & StaticImage { - height: 16px; - width: 16px; - cursor: pointer; - } - - & Loader { - width: 24px; - } - - & menu-bar-item-label { - display: none; - } - - & IconOrImage + menu-bar-item-label, - & Loader + menu-bar-item-label { - padding-left: 8px; - } - } - - MenuSeparator { - composes: theme-border-color-background from global; - height: 100%; - margin: 0; - border: 1px solid !important; - - &:first-child, - &:last-child { - display: none; - } - } -`; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_TOOLS_MENU.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_TOOLS_MENU.ts index fbd19d898e..ee79af3485 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_TOOLS_MENU.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_TOOLS_MENU.ts @@ -1,10 +1,10 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; -export const SQL_EDITOR_TOOLS_MENU = createMenu('sql-editor-tools', ''); +export const SQL_EDITOR_TOOLS_MENU = createMenu('sql-editor-tools', { label: '' }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_TOOLS_MORE_MENU.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_TOOLS_MORE_MENU.ts new file mode 100644 index 0000000000..c0e0b137cf --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_TOOLS_MORE_MENU.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { createMenu } from '@cloudbeaver/core-view'; + +export const SQL_EDITOR_TOOLS_MORE_MENU = createMenu('sql-editor-tools-more', { + label: '', + tooltip: 'sql_editor_tools_more_menu_tooltip', + icon: 'dots', +}); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/ScriptImportDialog.m.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/ScriptImportDialog.m.css deleted file mode 100644 index 88393af362..0000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/ScriptImportDialog.m.css +++ /dev/null @@ -1,9 +0,0 @@ -.footer { - align-items: center; -} - -.container { - width: 100%; - display: flex; - gap: 16px; -} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/ScriptImportDialog.module.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/ScriptImportDialog.module.css new file mode 100644 index 0000000000..ec20786d83 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/ScriptImportDialog.module.css @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.footer { + align-items: center; +} + +.container { + width: 100%; + display: flex; + gap: 16px; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/ScriptImportDialog.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/ScriptImportDialog.tsx index 513d0ff6c5..f2c90acaa2 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/ScriptImportDialog.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/ScriptImportDialog.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import { } from '@cloudbeaver/core-blocks'; import type { DialogComponent } from '@cloudbeaver/core-dialogs'; -import style from './ScriptImportDialog.m.css'; +import style from './ScriptImportDialog.module.css'; export const ScriptImportDialog: DialogComponent = function ScriptImportDialog({ resolveDialog, rejectDialog, className }) { const styles = useS(style); @@ -31,14 +31,14 @@ export const ScriptImportDialog: DialogComponent = function Scrip
- - -
diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx index e239468302..400e4efa31 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx @@ -1,149 +1,66 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { useEffect, useMemo, useState } from 'react'; -import styled, { css } from 'reshadow'; -import { getComputed, preventFocusHandler, StaticImage, useSplit, useTranslate } from '@cloudbeaver/core-blocks'; +import { getComputed, s, SContext, type StyleRegistry, useS, useSplit } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { BASE_TAB_STYLES, ITabData, TabList, TabPanelList, TabsState, VERTICAL_ROTATED_TAB_STYLES } from '@cloudbeaver/core-ui'; +import { type ITabData, TabList, TabListStyles, TabPanelList, TabsState, TabStyles } from '@cloudbeaver/core-ui'; import { MetadataMap } from '@cloudbeaver/core-utils'; import { useCaptureViewContext } from '@cloudbeaver/core-view'; -import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures'; -import { ISqlEditorModeProps, SqlEditorModeService } from '../SqlEditorModeService'; -import { DATA_CONTEXT_SQL_EDITOR_DATA } from './DATA_CONTEXT_SQL_EDITOR_DATA'; -import type { ISqlEditorProps } from './ISqlEditorProps'; -import { SqlEditorActionsMenu } from './SqlEditorActionsMenu'; -import { SqlEditorTools } from './SqlEditorTools'; -import { useSqlEditor } from './useSqlEditor'; - -const styles = css` - button, - upload { - composes: theme-ripple from global; - } - sql-editor { - position: relative; - z-index: 0; - flex: 1 auto; - height: 100%; - display: flex; - overflow: auto; - } - - container { - composes: theme-border-color-background from global; - display: flex; - flex-direction: column; - justify-content: space-between; - overflow: auto; - border-right: solid 1px; - } - - actions { - width: 32px; - display: flex; - flex-direction: column; - align-items: center; - user-select: none; - - &:empty { - width: initial; - } - } - - button, - upload { - background: none; - padding: 0; - margin: 0; - height: 32px; - width: 32px; - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - flex-shrink: 0; - } - - StaticImage { - height: 16px; - width: 16px; - cursor: pointer; - } -`; - -const tabStyles = css` - tabs { - composes: theme-background-secondary theme-text-on-secondary from global; - overflow-x: hidden; - padding-top: 4px; - } - Tab { - composes: theme-ripple theme-background-background theme-text-text-primary-on-light theme-typography--body2 from global; - text-transform: uppercase; - font-weight: normal; - - &:global([aria-selected='true']) { - font-weight: normal !important; - } - } - TabList { - composes: theme-background-secondary theme-text-on-secondary from global; - margin-right: 8px; - margin-left: 4px; - } -`; - -const tabListStyles = [BASE_TAB_STYLES, VERTICAL_ROTATED_TAB_STYLES, tabStyles]; +import { type ISqlEditorModeProps, SqlEditorModeService } from '../SqlEditorModeService.js'; +import { DATA_CONTEXT_SQL_EDITOR_DATA } from './DATA_CONTEXT_SQL_EDITOR_DATA.js'; +import type { ISqlEditorProps } from './ISqlEditorProps.js'; +import styles from './shared/SqlEditor.module.css'; +import SqlEditorTab from './shared/SqlEditorTab.module.css'; +import SqlEditorTabList from './shared/SqlEditorTabList.module.css'; +import { SQLEditorActions } from './SQLEditorActions.js'; +import { useSqlEditor } from './useSqlEditor.js'; +import { useActiveQuery } from './useActiveQuery.js'; + +const sqlEditorRegistry: StyleRegistry = [ + [TabListStyles, { mode: 'append', styles: [SqlEditorTabList] }], + [TabStyles, { mode: 'append', styles: [SqlEditorTab] }], +]; export const SqlEditor = observer(function SqlEditor({ state, className }) { - const translate = useTranslate(); const split = useSplit(); + const style = useS(styles, SqlEditorTab); const sqlEditorModeService = useService(SqlEditorModeService); const data = useSqlEditor(state); + useActiveQuery(data); const [modesState] = useState(() => new MetadataMap()); useMemo(() => { modesState.sync(state.modeState); }, [state]); - useCaptureViewContext(context => { - context?.set(DATA_CONTEXT_SQL_EDITOR_DATA, data); + useCaptureViewContext((context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_DATA, data, id); }); function handleModeSelect(tab: ITabData) { - state.currentModeId = tab.tabId; + data.setModeId(tab.tabId); } - const disabled = getComputed(() => data.isLineScriptEmpty || data.isDisabled); - const isActiveSegmentMode = getComputed(() => data.activeSegmentMode.activeSegmentMode); const displayedEditors = getComputed(() => sqlEditorModeService.tabsContainer.getDisplayed({ state, data }).length); + const isEditorEmpty = displayedEditors === 0; useEffect(() => { - if (displayedEditors === 0) { - split.fixate('maximize', true); + if (isEditorEmpty) { + split.state.setDisable(true); } else if (split.state.disable) { - split.fixate('resize', false); - split.state.setSize(-1); + split.state.setDisable(false); } - }); - - const isQuery = data.dataSource?.hasFeature(ESqlDataSourceFeatures.query); - const isExecutable = data.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); + }, [isEditorEmpty]); - return styled( - styles, - BASE_TAB_STYLES, - VERTICAL_ROTATED_TAB_STYLES, - tabStyles, - )( + return ( (function SqlEditor({ state, c lazy onChange={handleModeSelect} > - - - - {isExecutable && ( - <> - {isQuery && ( - <> - - - - )} - - {isQuery && data.dialect?.supportsExplainExecutionPlan && ( - - )} - - )} - - - - - - {displayedEditors > 1 ? ( - - - - ) : null} - - , + +
+ + + {displayedEditors > 1 ? ( +
+ +
+ ) : null} +
+
+ ); }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenu.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenu.tsx index f33f54e54b..f2ad468081 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenu.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenu.tsx @@ -1,21 +1,21 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { s, SContext, StyleRegistry, useS } from '@cloudbeaver/core-blocks'; +import { s, SContext, type StyleRegistry, useS } from '@cloudbeaver/core-blocks'; import type { IDataContext } from '@cloudbeaver/core-data-context'; import { MenuBar, MenuBarItemStyles, MenuBarStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; -import type { ISqlEditorTabState } from '../ISqlEditorTabState'; -import { SQL_EDITOR_ACTIONS_MENU } from './SQL_EDITOR_ACTIONS_MENU'; -import SqlEditorActionsMenuBarStyles from './SqlEditorActionsMenuBar.m.css'; -import SqlEditorActionsMenuBarItemStyles from './SqlEditorActionsMenuBarItem.m.css'; +import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; +import { SQL_EDITOR_ACTIONS_MENU } from './SQL_EDITOR_ACTIONS_MENU.js'; +import SqlEditorActionsMenuBarStyles from './SqlEditorActionsMenuBar.module.css'; +import SqlEditorActionsMenuBarItemStyles from './SqlEditorActionsMenuBarItem.module.css'; interface Props { state: ISqlEditorTabState; @@ -41,12 +41,12 @@ const registry: StyleRegistry = [ ]; export const SqlEditorActionsMenu = observer(function SqlEditorActionsMenu({ state, context, className }) { - const styles = useS(SqlEditorActionsMenuBarStyles, SqlEditorActionsMenuBarItemStyles); + const menuBarStyles = useS(SqlEditorActionsMenuBarStyles, SqlEditorActionsMenuBarItemStyles, MenuBarStyles, MenuBarItemStyles); const menu = useMenu({ menu: SQL_EDITOR_ACTIONS_MENU, context }); return ( - + ); }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.m.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.m.css deleted file mode 100644 index 6622623401..0000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.m.css +++ /dev/null @@ -1,4 +0,0 @@ -.sqlActions.menuBar { - height: unset; - width: 32px; -} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.module.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.module.css new file mode 100644 index 0000000000..15332234bf --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.module.css @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.sqlActions { + display: flex; + flex-direction: column; +} + +.sqlActions.menuBar { + height: unset; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBarItem.m.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBarItem.m.css deleted file mode 100644 index e2bce2142b..0000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBarItem.m.css +++ /dev/null @@ -1,7 +0,0 @@ -.sqlActions .menuBarItem { - padding: 4px; -} - -.sqlActions .menuBarItemLabel { - display: none; -} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBarItem.module.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBarItem.module.css new file mode 100644 index 0000000000..49bed26a12 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBarItem.module.css @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.toolsMenu .menuBarItemGroup .menuBarItem { + & .menuBarItemLabel { + display: none; + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorLoader.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorLoader.tsx index a02d88444d..1734a2239e 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorLoader.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorLoader.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,10 +9,10 @@ import { observer } from 'mobx-react-lite'; import { ComplexLoader, createComplexLoader } from '@cloudbeaver/core-blocks'; -import type { ISqlEditorProps } from './ISqlEditorProps'; +import type { ISqlEditorProps } from './ISqlEditorProps.js'; const loader = createComplexLoader(async function loader() { - const { SqlEditor } = await import('./SqlEditor'); + const { SqlEditor } = await import('./SqlEditor.js'); return { SqlEditor }; }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorStateContext.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorStateContext.ts new file mode 100644 index 0000000000..927eabf76c --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorStateContext.ts @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { ISQLEditorData } from './ISQLEditorData.js'; + +interface ISqlEditorStateContext { + state: ISQLEditorData | null; + setState: (state: ISQLEditorData | null) => void; +} + +export function SqlEditorStateContext(): ISqlEditorStateContext { + return { + state: null, + setState(state) { + this.state = state; + }, + }; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorTools.module.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorTools.module.css new file mode 100644 index 0000000000..5e9447df6f --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorTools.module.css @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tools { + display: flex; + flex-direction: column; + align-items: center; + + &:empty { + display: none; + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorTools.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorTools.tsx index f71d390db6..7a02253684 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorTools.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorTools.tsx @@ -1,113 +1,31 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 styled, { css } from 'reshadow'; -import { getComputed, preventFocusHandler, StaticImage, UploadArea, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; -import type { ComponentStyle } from '@cloudbeaver/core-theming'; +import { preventFocusHandler, s, useS } from '@cloudbeaver/core-blocks'; -import type { ISqlEditorTabState } from '../ISqlEditorTabState'; -import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures'; -import type { ISQLEditorData } from './ISQLEditorData'; -import { SqlEditorToolsMenu } from './SqlEditorToolsMenu'; -import { useTools } from './useTools'; - -const styles = css` - tools { - width: 32px; - display: flex; - flex-direction: column; - align-items: center; - - &:empty { - display: none; - } - } -`; +import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; +import type { ISQLEditorData } from './ISQLEditorData.js'; +import style from './SqlEditorTools.module.css'; +import { SqlEditorToolsMenu } from './SqlEditorToolsMenu.js'; interface Props { data: ISQLEditorData; state: ISqlEditorTabState; - style?: ComponentStyle; className?: string; } -export const SqlEditorTools = observer(function SqlEditorTools({ data, state, style, className }) { - const translate = useTranslate(); - const tools = useTools(state); - const scriptEmpty = getComputed(() => data.value.length === 0); - const disabled = getComputed(() => data.isDisabled || data.isScriptEmpty); - const isActiveSegmentMode = getComputed(() => data.activeSegmentMode.activeSegmentMode); - - async function handleScriptUpload(event: React.ChangeEvent) { - const file = event.target.files?.[0]; - - if (!file) { - throw new Error('File is not found'); - } - - const prevScript = data.value.trim(); - const script = await tools.tryReadScript(file, prevScript); - - if (script) { - data.setQuery(script); - } - } - - async function downloadScriptHandler() { - tools.downloadScript(data.value.trim()); - } - - const isScript = data.dataSource?.hasFeature(ESqlDataSourceFeatures.script); +export const SqlEditorTools = observer(function SqlEditorTools({ data, state, className }) { + const styles = useS(style); - return styled(useStyles(style, styles))( - - - {isScript && ( - <> - - - {!isActiveSegmentMode && ( - - - - - - )} - {/**/} - - )} - , + return ( +
+ +
); }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx index f9961ea9d7..e168cbbabd 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,17 +8,20 @@ import { observer } from 'mobx-react-lite'; import { s, SContext, type StyleRegistry, useS } from '@cloudbeaver/core-blocks'; -import type { IDataContext } from '@cloudbeaver/core-data-context'; +import { type IDataContext, useDataContextLink } from '@cloudbeaver/core-data-context'; import { MenuBar, MenuBarItemStyles, MenuBarStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; -import { DATA_CONTEXT_SQL_EDITOR_STATE } from '../DATA_CONTEXT_SQL_EDITOR_STATE'; -import type { ISqlEditorTabState } from '../ISqlEditorTabState'; -import { SQL_EDITOR_TOOLS_MENU } from './SQL_EDITOR_TOOLS_MENU'; -import SqlEditorActionsMenuBarStyles from './SqlEditorActionsMenuBar.m.css'; -import SqlEditorActionsMenuBarItemStyles from './SqlEditorActionsMenuBarItem.m.css'; +import { DATA_CONTEXT_SQL_EDITOR_STATE } from '../DATA_CONTEXT_SQL_EDITOR_STATE.js'; +import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; +import { DATA_CONTEXT_SQL_EDITOR_DATA } from './DATA_CONTEXT_SQL_EDITOR_DATA.js'; +import type { ISQLEditorData } from './ISQLEditorData.js'; +import { SQL_EDITOR_TOOLS_MENU } from './SQL_EDITOR_TOOLS_MENU.js'; +import SqlEditorActionsMenuBarStyles from './SqlEditorActionsMenuBar.module.css'; +import SqlEditorActionsMenuBarItemStyles from './SqlEditorActionsMenuBarItem.module.css'; interface Props { + data: ISQLEditorData; state: ISqlEditorTabState; context?: IDataContext; className?: string; @@ -40,14 +43,20 @@ const registry: StyleRegistry = [ }, ], ]; -export const SqlEditorToolsMenu = observer(function SqlEditorToolsMenu({ state, context, className }) { - const styles = useS(SqlEditorActionsMenuBarStyles, SqlEditorActionsMenuBarItemStyles); +export const SqlEditorToolsMenu = observer(function SqlEditorToolsMenu({ data, state, context, className }) { + const menuBarStyles = useS(SqlEditorActionsMenuBarStyles, SqlEditorActionsMenuBarItemStyles, MenuBarStyles, MenuBarItemStyles); const menu = useMenu({ menu: SQL_EDITOR_TOOLS_MENU, context }); - menu.context.set(DATA_CONTEXT_SQL_EDITOR_STATE, state); + + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_STATE, state, id); + }); + useDataContextLink(menu.context, (context, id) => { + context.set(DATA_CONTEXT_SQL_EDITOR_DATA, data, id); + }); return ( - + ); }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/shared/SqlEditor.module.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/shared/SqlEditor.module.css new file mode 100644 index 0000000000..bd8cfaca71 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/shared/SqlEditor.module.css @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.sqlEditor { + position: relative; + z-index: 0; + flex: 1 auto; + height: 100%; + display: flex; + overflow: auto; +} + +.tabs { + composes: theme-background-secondary theme-text-on-secondary from global; + overflow-x: hidden; + padding-top: 4px; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/shared/SqlEditorTab.module.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/shared/SqlEditorTab.module.css new file mode 100644 index 0000000000..eb9675cace --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/shared/SqlEditorTab.module.css @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.tab { + composes: theme-ripple theme-background-background theme-text-text-primary-on-light theme-typography--body2 from global; +} +.sqlEditor .tab { + text-transform: uppercase; + font-weight: normal; + + &:global([aria-selected='true']) { + font-weight: normal !important; + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/shared/SqlEditorTabList.module.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/shared/SqlEditorTabList.module.css new file mode 100644 index 0000000000..a57db7c7fb --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/shared/SqlEditorTabList.module.css @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tabList { + composes: theme-background-secondary theme-text-on-secondary from global; + margin-right: 8px; + margin-left: 4px; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useActiveQuery.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useActiveQuery.ts new file mode 100644 index 0000000000..3d449d135a --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useActiveQuery.ts @@ -0,0 +1,87 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { useService } from '@cloudbeaver/core-di'; +import type { ISQLEditorData } from './ISQLEditorData.js'; +import { SqlEditorService, type ISqlEditorActiveQueryUpdateData } from '../SqlEditorService.js'; +import { useExecutor } from '@cloudbeaver/core-blocks'; +import { useState } from 'react'; +import { ExecutorHandlersCollection } from '@cloudbeaver/core-executor'; +import { SqlEditorStateContext } from './SqlEditorStateContext.js'; + +export function useActiveQuery(state: ISQLEditorData): void { + const [collection] = useState(() => new ExecutorHandlersCollection()); + const sqlEditorService = useService(SqlEditorService); + + useExecutor({ + executor: collection, + handlers: [ + function fillEditorState(data, context) { + const sqlEditorStateContext = context.getContext(SqlEditorStateContext); + sqlEditorStateContext.setState(state); + }, + ], + }); + + useExecutor({ + executor: sqlEditorService.updateActiveQuery, + before: collection, + handlers: [ + async function updateActiveQuery(data, context) { + const sqlEditorStateContext = context.getContext(SqlEditorStateContext); + const sqlEditorData = sqlEditorStateContext.state; + + if (!sqlEditorData) { + return; + } + + const segment = await sqlEditorData.getResolvedSegment(); + let query = data.update.query.trim(); + + if (segment) { + query = query.trim(); + // TODO: getResolvedSegment will return segment without end delimiter + // we need this to avoid duplicated end delimiter + const dialectDelimiter = sqlEditorData.dialect?.scriptDelimiter ?? ''; + const endDelimiterLength = dialectDelimiter.length; + const endDelimiter = sqlEditorData.value.slice(segment.end, segment.end + endDelimiterLength); + + if (endDelimiter === dialectDelimiter && query.endsWith(endDelimiter)) { + query = query.slice(0, -endDelimiterLength); + } + } + + switch (data.update.type) { + case 'replace': + if (segment) { + const firstQueryPart = sqlEditorData.value.slice(0, segment.begin) + query; + sqlEditorData.setScript(firstQueryPart + sqlEditorData.value.slice(segment.end), undefined, { + anchor: firstQueryPart.length, + head: firstQueryPart.length, + }); + } else { + sqlEditorData.setScript(query, undefined, sqlEditorData.cursor); + } + break; + case 'append': + { + const newScript = sqlEditorData.value + query; + sqlEditorData.setScript(newScript, undefined, { anchor: newScript.length, head: newScript.length }); + } + break; + case 'prepend': + { + const newScript = query + sqlEditorData.value; + sqlEditorData.setScript(newScript, undefined, { anchor: 0, head: 0 }); + } + break; + } + }, + ], + }); +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index 1b275724b8..5eed008ea8 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -1,11 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { action, autorun, computed, IReactionDisposer, observable, untracked } from 'mobx'; +import { action, autorun, computed, type IReactionDisposer, observable, runInAction, untracked } from 'mobx'; import { useEffect } from 'react'; import { ConfirmationDialog, useExecutor, useObservableRef } from '@cloudbeaver/core-blocks'; @@ -15,22 +15,22 @@ import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dial import { NotificationService } from '@cloudbeaver/core-events'; import { SyncExecutor } from '@cloudbeaver/core-executor'; import type { SqlCompletionProposal, SqlDialectInfo, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk'; -import { createLastPromiseGetter, LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils'; - -import type { ISqlEditorTabState } from '../ISqlEditorTabState'; -import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures'; -import type { ISqlDataSource } from '../SqlDataSource/ISqlDataSource'; -import { SqlDataSourceService } from '../SqlDataSource/SqlDataSourceService'; -import { SqlDialectInfoService } from '../SqlDialectInfoService'; -import { SqlEditorService } from '../SqlEditorService'; -import { ISQLScriptSegment, SQLParser } from '../SQLParser'; -import { SqlExecutionPlanService } from '../SqlResultTabs/ExecutionPlan/SqlExecutionPlanService'; -import { OUTPUT_LOGS_TAB_ID } from '../SqlResultTabs/OutputLogs/OUTPUT_LOGS_TAB_ID'; -import { OutputLogsService } from '../SqlResultTabs/OutputLogs/OutputLogsService'; -import { SqlQueryService } from '../SqlResultTabs/SqlQueryService'; -import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService'; -import type { ICursor, ISQLEditorData } from './ISQLEditorData'; -import { ISQLEditorMode, SQLEditorModeContext } from './SQLEditorModeContext'; +import { createLastPromiseGetter, debounceAsync, type LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils'; + +import type { ISqlEditorTabState } from '../ISqlEditorTabState.js'; +import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures.js'; +import type { ISqlDataSource, ISqlEditorCursor } from '../SqlDataSource/ISqlDataSource.js'; +import { SqlDataSourceService } from '../SqlDataSource/SqlDataSourceService.js'; +import { SqlDialectInfoService } from '../SqlDialectInfoService.js'; +import { SqlEditorService } from '../SqlEditorService.js'; +import { type ISQLScriptSegment, SQLParser } from '../SQLParser.js'; +import { SqlExecutionPlanService } from '../SqlResultTabs/ExecutionPlan/SqlExecutionPlanService.js'; +import { OUTPUT_LOGS_TAB_ID } from '../SqlResultTabs/OutputLogs/OUTPUT_LOGS_TAB_ID.js'; +import { SqlQueryService } from '../SqlResultTabs/SqlQueryService.js'; +import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService.js'; +import type { ISQLEditorData } from './ISQLEditorData.js'; +import { SQLEditorModeContext } from './SQLEditorModeContext.js'; +import { SqlEditorSettingsService } from '../SqlEditorSettingsService.js'; interface ISQLEditorDataPrivate extends ISQLEditorData { readonly sqlDialectInfoService: SqlDialectInfoService; @@ -39,13 +39,14 @@ interface ISQLEditorDataPrivate extends ISQLEditorData { readonly sqlEditorService: SqlEditorService; readonly notificationService: NotificationService; readonly sqlExecutionPlanService: SqlExecutionPlanService; + readonly sqlEditorSettingsService: SqlEditorSettingsService; readonly commonDialogService: CommonDialogService; readonly sqlResultTabsService: SqlResultTabsService; readonly dataSource: ISqlDataSource | undefined; readonly getLastAutocomplete: LastPromiseGetter; readonly parseScript: LastPromiseGetter; - cursor: ICursor; + cursor: ISqlEditorCursor; readonlyState: boolean; executingScript: boolean; state: ISqlEditorTabState; @@ -54,7 +55,6 @@ interface ISQLEditorDataPrivate extends ISQLEditorData { updateParserScripts(): Promise; loadDatabaseDataModels(): Promise; getExecutingQuery(script: boolean): ISQLScriptSegment | undefined; - getResolvedSegment(): Promise; getSubQuery(): ISQLScriptSegment | undefined; } @@ -70,7 +70,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { const sqlResultTabsService = useService(SqlResultTabsService); const commonDialogService = useService(CommonDialogService); const sqlDataSourceService = useService(SqlDataSourceService); - const sqlOutputLogsService = useService(OutputLogsService); + const sqlEditorSettingsService = useService(SqlEditorSettingsService); const data = useObservableRef( () => ({ @@ -86,11 +86,9 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { return this.sqlDialectInfoService.getDialectInfo(createConnectionParam(executionContext.projectId, executionContext.connectionId)); }, - get activeSegmentMode(): ISQLEditorMode { - const contexts = this.onMode.execute(this); - const mode = contexts.getContext(SQLEditorModeContext); - - return mode; + activeSegmentMode: { + activeSegment: undefined, + activeSegmentMode: false, }, get activeSegment(): ISQLScriptSegment | undefined { @@ -98,23 +96,19 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, get cursorSegment(): ISQLScriptSegment | undefined { - return this.parser.getSegment(this.cursor.begin, -1); + return this.parser.getSegment(this.cursor.anchor, -1); }, get readonly(): boolean { - return this.executingScript || this.readonlyState || !!this.dataSource?.isOutdated() || !!this.dataSource?.isReadonly() || !this.editing; + return this.executingScript || this.readonlyState || !!this.dataSource?.isReadonly() || !this.editing; }, get editing(): boolean { return this.dataSource?.isEditing() ?? false; }, - get isLineScriptEmpty(): boolean { - return !this.activeSegment?.query; - }, - get isScriptEmpty(): boolean { - return this.value === '' || this.parser.scripts.length === 0; + return this.value === ''; }, get isDisabled(): boolean { @@ -131,6 +125,10 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { return this.dataSource?.isIncomingChanges ?? false; }, + get cursor(): ISqlEditorCursor { + return this.dataSource?.cursor ?? { anchor: 0, head: 0 }; + }, + get value(): string { return this.dataSource?.script ?? ''; }, @@ -139,14 +137,16 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { return this.dataSource?.incomingScript; }, + get isExecutionAllowed(): boolean { + return !!this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) && this.sqlEditorSettingsService.scriptExecutionEnabled; + }, + onMode: new SyncExecutor(), onExecute: new SyncExecutor(), onSegmentExecute: new SyncExecutor(), onUpdate: new SyncExecutor(), - onFormat: new SyncExecutor(), parser: new SQLParser(), - cursor: { begin: 0, end: 0 }, readonlyState: false, executingScript: false, reactionDisposer: null, @@ -169,7 +169,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { untracked(() => { this.sqlDialectInfoService.loadSqlDialectInfo(key).then(async () => { try { - await this.updateParserScriptsThrottle(); + await this.updateParserScriptsDebounced(); } catch {} }); }); @@ -183,11 +183,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, setCursor(begin: number, end = begin): void { - this.cursor = { - begin, - end, - }; - this.onUpdate.execute(); + this.dataSource?.setCursor(begin, end); }, getLastAutocomplete: createLastPromiseGetter(), @@ -200,6 +196,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { } const hints = await this.sqlEditorService.getAutocomplete( + executionContext.projectId, executionContext.connectionId, executionContext.id, this.value, @@ -211,13 +208,14 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { this.hintsLimitIsMet = hints.length >= MAX_HINTS_LIMIT; return hints; - }, 1000 / 3), + }, 300), async formatScript(): Promise { if (this.isDisabled || this.isScriptEmpty || !this.dataSource?.executionContext) { return; } + await this.updateParserScripts(); const query = this.value; const script = this.getExecutingQuery(false); @@ -230,8 +228,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { this.readonlyState = true; const formatted = await this.sqlDialectInfoService.formatScript(this.dataSource.executionContext, script.query); - this.onFormat.execute([script, formatted]); - this.setQuery(query.substring(0, script.begin) + formatted + query.substring(script.end)); + this.setScript(query.substring(0, script.begin) + formatted + query.substring(script.end)); + this.setCursor(script.begin + formatted.length); } finally { this.readonlyState = false; } @@ -239,11 +237,12 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { async executeQuery(): Promise { const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query); - const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); - if (!isQuery || !isExecutable) { + if (!isQuery || !this.isExecutionAllowed) { return; } + + await this.updateParserScripts(); const query = this.getSubQuery(); try { @@ -258,7 +257,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { await this.executeQueryAction( query, - async () => { + () => { if (this.dataSource?.databaseModels.length) { this.sqlQueryService.initDatabaseDataModels(this.state); } @@ -270,11 +269,12 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { async executeQueryNewTab(): Promise { const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query); - const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); - if (!isQuery || !isExecutable) { + if (!isQuery || !this.isExecutionAllowed) { return; } + + await this.updateParserScripts(); const query = this.getSubQuery(); try { @@ -286,12 +286,12 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { async showExecutionPlan(): Promise { const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query); - const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); - if (!isQuery || !isExecutable || !this.dialect?.supportsExplainExecutionPlan) { + if (!isQuery || !this.isExecutionAllowed || !this.dialect?.supportsExplainExecutionPlan) { return; } + await this.updateParserScripts(); const query = this.getSubQuery(); try { @@ -301,20 +301,18 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { } catch {} }, - async switchEditing(): Promise { + switchEditing(): void { this.dataSource?.setEditing(!this.dataSource.isEditing()); }, async executeScript(): Promise { - const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); - - if (!isExecutable || this.isDisabled || this.isScriptEmpty) { + if (!this.isExecutionAllowed || this.isDisabled || this.isScriptEmpty) { return; } - if (this.state.tabs.length) { - const processableTabs = this.state.tabs.filter(tab => tab.id !== OUTPUT_LOGS_TAB_ID); + const processableTabs = this.state.tabs.filter(tab => tab.id !== OUTPUT_LOGS_TAB_ID); + if (processableTabs.length > 0) { const result = await this.commonDialogService.open(ConfirmationDialog, { title: 'sql_editor_close_result_tabs_dialog_title', message: `Do you want to close ${processableTabs.length} tabs before executing script?`, @@ -346,11 +344,11 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { queries.map(query => query.query), { onQueryExecutionStart: (query, index) => { - const segment = queries[index]; + const segment = queries[index]!; this.onSegmentExecute.execute({ segment, type: 'start' }); }, onQueryExecuted: (query, index, success) => { - const segment = queries[index]; + const segment = queries[index]!; this.onSegmentExecute.execute({ segment, type: 'end' }); if (!success) { @@ -364,37 +362,41 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { } }, - setQuery(query: string): void { - this.sqlEditorService.setQuery(query, this.state); - this.updateParserScriptsThrottle().catch(() => {}); + setScript(query: string, source?: string, cursor?: ISqlEditorCursor): void { + this.dataSource?.setScript(query, source, cursor); }, - updateParserScriptsThrottle: throttleAsync(async function updateParserScriptsThrottle() { + updateParserScriptsDebounced: debounceAsync(async function updateParserScriptsThrottle() { await data.updateParserScripts(); - }, 1000 / 2), + }, 2000), async updateParserScripts() { if (!this.dataSource?.hasFeature(ESqlDataSourceFeatures.script)) { return; } + const projectId = this.dataSource.executionContext?.projectId; const connectionId = this.dataSource.executionContext?.connectionId; const script = this.parser.actualScript; - if (!connectionId || !script) { + if (!projectId || !connectionId || !script) { + this.parser.setQueries([]); + this.onUpdate.execute(); return; } const { queries } = await this.parseScript([connectionId, script], async () => { try { - return await this.sqlEditorService.parseSQLScript(connectionId, script); + return await this.sqlEditorService.parseSQLScript(projectId, connectionId, script); } catch (exception: any) { this.notificationService.logException(exception, 'Failed to parse SQL script'); throw exception; } }); + // check if script was changed while we were waiting for response if (this.parser.actualScript === script) { this.parser.setQueries(queries); + this.onUpdate.execute(); } }, @@ -404,19 +406,20 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { passEmpty?: boolean, passDisabled?: boolean, ): Promise { - if (!segment || (this.isDisabled && !passDisabled) || (!passEmpty && this.isLineScriptEmpty)) { + if (!segment || segment.end === segment.begin || (this.isDisabled && !passDisabled) || (!passEmpty && this.isScriptEmpty)) { return; } this.onExecute.execute(true); + const id = setTimeout(() => this.onSegmentExecute.execute({ segment, type: 'start' }), 250); try { - const id = setTimeout(() => this.onSegmentExecute.execute({ segment, type: 'start' }), 250); const result = await action(segment); clearTimeout(id); this.onSegmentExecute.execute({ segment, type: 'end' }); return result; } catch (exception: any) { + clearTimeout(id); this.onSegmentExecute.execute({ segment, type: 'end' }); this.onSegmentExecute.execute({ segment, type: 'error' }); throw exception; @@ -432,25 +435,38 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, async getResolvedSegment(): Promise { + const projectId = this.dataSource?.executionContext?.projectId; const connectionId = this.dataSource?.executionContext?.connectionId; - if (!connectionId || this.cursor.begin !== this.cursor.end) { - return this.getSubQuery(); - } + while (true) { + const currentScript = this.parser.actualScript; + // TODO: we updating parser scripts + // script may be changed this will lead to temporary wrong segments offsets + await data.updateParserScripts(); + if (currentScript !== this.parser.actualScript) { + continue; + } - if (this.activeSegmentMode.activeSegmentMode) { - return this.activeSegment; - } + if (!projectId || !connectionId || this.cursor.anchor !== this.cursor.head) { + return this.getSubQuery(); + } - const result = await this.sqlEditorService.parseSQLQuery(connectionId, this.value, this.cursor.begin); + if (this.activeSegmentMode.activeSegmentMode) { + return this.activeSegment; + } - const segment = this.parser.getSegment(result.start, result.end); + const result = await this.sqlEditorService.parseSQLQuery(projectId, connectionId, currentScript, this.cursor.anchor); + if (currentScript !== this.parser.actualScript) { + continue; + } + if (result.end === 0 && result.start === 0) { + return this.cursorSegment; + } - if (!segment) { - throw new Error('Failed to get position'); + // TODO: here we use parser that may be outdated and segment will return wrong value + const segment = this.parser.getSegment(result.start, result.end); + return segment; } - - return segment; }, getSubQuery(): ISQLScriptSegment | undefined { @@ -464,8 +480,13 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { return query; }, + setModeId(tabId: string): void { + this.state.currentModeId = tabId; + this.onUpdate.execute(); + }, }), { + getHintProposals: action.bound, formatScript: action.bound, executeQuery: action.bound, executeQueryNewTab: action.bound, @@ -473,14 +494,15 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { executeScript: action.bound, switchEditing: action.bound, dialect: computed, - isLineScriptEmpty: computed, isDisabled: computed, value: computed, readonly: computed, + cursor: computed, + activeSegmentMode: observable.ref, hintsLimitIsMet: observable.ref, - cursor: observable, readonlyState: observable, executingScript: observable, + sqlEditorSettingsService: observable.ref, }, { state, @@ -492,6 +514,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { sqlResultTabsService, notificationService, commonDialogService, + sqlEditorSettingsService, }, ); @@ -502,6 +525,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { handlers: [ function setScript({ script }) { data.parser.setScript(script); + data.updateParserScriptsDebounced().catch(() => {}); data.onUpdate.execute(); }, ], @@ -516,6 +540,33 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { ], }); + useExecutor({ + executor: data.onUpdate, + handlers: [ + function updateActiveSegmentMode() { + // Probably we need to rework this logic + // we want to track active segment mode with mobx + // right now it's leads to bag when script changed from empty to not empty + // data.isLineScriptEmpty skips this change + const contexts = data.onMode.execute(data); + data.activeSegmentMode = contexts.getContext(SQLEditorModeContext); + }, + ], + }); + + useEffect(() => { + const subscription = autorun(() => { + const contexts = data.onMode.execute(data); + const activeSegmentMode = contexts.getContext(SQLEditorModeContext); + + runInAction(() => { + data.activeSegmentMode = activeSegmentMode; + }); + }); + + return subscription; + }, [data]); + useEffect(() => () => data.destruct(), []); return data; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx deleted file mode 100644 index c7899727c9..0000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx +++ /dev/null @@ -1,130 +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 { action } from 'mobx'; - -import { useObservableRef } from '@cloudbeaver/core-blocks'; -import { Connection, ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { download, generateFileName, getTextFileReadingProcess } from '@cloudbeaver/core-utils'; - -import { getSqlEditorName } from '../getSqlEditorName'; -import type { ISqlEditorTabState } from '../ISqlEditorTabState'; -import { SqlDataSourceService } from '../SqlDataSource/SqlDataSourceService'; -import { SqlEditorSettingsService } from '../SqlEditorSettingsService'; -import { ScriptImportDialog } from './ScriptImportDialog'; - -interface State { - tryReadScript: (file: File, prevScript: string) => Promise; - readScript: (file: File) => Promise; - downloadScript: (script: string) => void; - checkFileValidity: (file: File) => boolean; -} - -export function useTools(state: ISqlEditorTabState): Readonly { - const commonDialogService = useService(CommonDialogService); - const notificationService = useService(NotificationService); - const connectionInfoResource = useService(ConnectionInfoResource); - const sqlEditorSettingsService = useService(SqlEditorSettingsService); - const sqlDataSourceService = useService(SqlDataSourceService); - - return useObservableRef( - () => ({ - async tryReadScript(file: File, prevScript: string) { - const valid = this.checkFileValidity(file); - - if (!valid) { - return null; - } - - if (prevScript) { - const result = await this.commonDialogService.open(ScriptImportDialog, null); - - if (result === DialogueStateResult.Rejected) { - return null; - } - - if (result !== DialogueStateResult.Resolved && result) { - this.downloadScript(prevScript); - } - } - - return this.readScript(file); - }, - - async readScript(file: File) { - let script = null; - try { - const process = getTextFileReadingProcess(file); - script = await process.promise; - } catch (exception: any) { - this.notificationService.logException(exception, 'Uploading script error'); - } - - return script; - }, - - checkFileValidity(file: File) { - const maxSize = this.sqlEditorSettingsService.settings.isValueDefault('maxFileSize') - ? this.sqlEditorSettingsService.deprecatedSettings.getValue('maxFileSize') - : this.sqlEditorSettingsService.settings.getValue('maxFileSize'); - - const size = Math.round(file.size / 1024); // kilobyte - const aboveMaxSize = size > maxSize; - - if (aboveMaxSize) { - this.notificationService.logInfo({ - title: 'sql_editor_upload_script_max_size_title', - message: `Max size: ${maxSize}KB\nFile size: ${size}KB`, - autoClose: false, - }); - - return false; - } - - return true; - }, - - downloadScript(script: string) { - if (!script.trim()) { - return; - } - - const blob = new Blob([script], { - type: 'application/sql', - }); - - const dataSource = sqlDataSourceService.get(this.state.editorId); - const executionContext = dataSource?.executionContext; - let connection: Connection | undefined; - - if (executionContext) { - connection = this.connectionInfoResource.get(createConnectionParam(executionContext.projectId, executionContext.connectionId)); - } - - const name = getSqlEditorName(this.state, dataSource, connection); - - download(blob, generateFileName(name, '.sql')); - }, - }), - { - tryReadScript: action.bound, - readScript: action.bound, - checkFileValidity: action.bound, - downloadScript: action.bound, - }, - { - commonDialogService, - connectionInfoResource, - notificationService, - sqlEditorSettingsService, - state, - }, - ); -} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorGroupTabsBootstrap.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorGroupTabsBootstrap.ts new file mode 100644 index 0000000000..1617560d64 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorGroupTabsBootstrap.ts @@ -0,0 +1,82 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { IDataContextProvider } from '@cloudbeaver/core-data-context'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { DATA_CONTEXT_TABS_CONTEXT, MENU_TAB } from '@cloudbeaver/core-ui'; +import { ActionService, menuExtractItems, MenuService } from '@cloudbeaver/core-view'; + +import { ACTION_TAB_CLOSE_SQL_RESULT_GROUP } from './ACTION_TAB_CLOSE_SQL_RESULT_GROUP.js'; +import { DATA_CONTEXT_SQL_EDITOR_STATE } from './DATA_CONTEXT_SQL_EDITOR_STATE.js'; +import { DATA_CONTEXT_SQL_EDITOR_RESULT_ID } from './SqlResultTabs/DATA_CONTEXT_SQL_EDITOR_RESULT_ID.js'; +import { SqlResultTabsService } from './SqlResultTabs/SqlResultTabsService.js'; + +@injectable(() => [ActionService, MenuService, SqlResultTabsService]) +export class SqlEditorGroupTabsBootstrap extends Bootstrap { + constructor( + private readonly actionService: ActionService, + private readonly menuService: MenuService, + private readonly sqlResultTabsService: SqlResultTabsService, + ) { + super(); + } + + override register(): void { + this.menuService.addCreator({ + menus: [MENU_TAB], + contexts: [DATA_CONTEXT_SQL_EDITOR_RESULT_ID, DATA_CONTEXT_SQL_EDITOR_STATE], + isApplicable: context => { + const state = context.get(DATA_CONTEXT_TABS_CONTEXT); + return !!state?.enabledBaseActions; + }, + getItems: (context, items) => [...items, ACTION_TAB_CLOSE_SQL_RESULT_GROUP], + orderItems: (context, items) => { + items.push(...menuExtractItems(items, [ACTION_TAB_CLOSE_SQL_RESULT_GROUP])); + return items; + }, + }); + + this.actionService.addHandler({ + id: 'result-tabs-group-base-handler', + actions: [ACTION_TAB_CLOSE_SQL_RESULT_GROUP], + menus: [MENU_TAB], + contexts: [DATA_CONTEXT_SQL_EDITOR_RESULT_ID, DATA_CONTEXT_SQL_EDITOR_STATE, DATA_CONTEXT_TABS_CONTEXT], + isActionApplicable: context => { + const tab = context.get(DATA_CONTEXT_SQL_EDITOR_RESULT_ID)!; + const sqlEditorState = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + + const groupId = sqlEditorState?.resultTabs.find(tabState => tabState.tabId === tab.id)?.groupId; + const hasTabsInGroup = sqlEditorState.resultTabs.filter(tabState => tabState.groupId === groupId).length > 1; + + return hasTabsInGroup; + }, + handler: async (context, action) => { + switch (action) { + case ACTION_TAB_CLOSE_SQL_RESULT_GROUP: + this.closeResultTabGroup(context); + break; + default: + break; + } + }, + }); + } + + async closeResultTabGroup(context: IDataContextProvider) { + const tab = context.get(DATA_CONTEXT_SQL_EDITOR_RESULT_ID)!; + const sqlEditorState = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; + const tabsContext = context.get(DATA_CONTEXT_TABS_CONTEXT)!; + const resultTabs = this.sqlResultTabsService.getResultTabs(sqlEditorState); + + const resultTab = resultTabs.find(tabState => tabState.tabId === tab.id); + const groupResultTabs = resultTabs.filter(tab => tab.groupId === resultTab?.groupId); + + groupResultTabs.forEach(groupTab => { + tabsContext.close(groupTab.tabId); + }); + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorModeService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorModeService.ts index d23f7ecc74..0f552bfe46 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorModeService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorModeService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,8 +8,8 @@ import { injectable } from '@cloudbeaver/core-di'; import { TabsContainer } from '@cloudbeaver/core-ui'; -import type { ISqlEditorTabState } from './ISqlEditorTabState'; -import type { ISQLEditorData } from './SqlEditor/ISQLEditorData'; +import type { ISqlEditorTabState } from './ISqlEditorTabState.js'; +import type { ISQLEditorData } from './SqlEditor/ISQLEditorData.js'; export interface ISqlEditorModeProps { state: ISqlEditorTabState; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorOpenOverlay.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditorOpenOverlay.tsx new file mode 100644 index 0000000000..9851e46a3e --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorOpenOverlay.tsx @@ -0,0 +1,51 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { + Button, + Fill, + Overlay, + OverlayActions, + OverlayHeader, + OverlayHeaderIcon, + OverlayHeaderTitle, + OverlayMessage, + useTranslate, +} from '@cloudbeaver/core-blocks'; + +import type { ISqlDataSource } from './SqlDataSource/ISqlDataSource.js'; + +interface Props { + dataSource: ISqlDataSource | undefined; +} + +// TODO: probably we need to combine this component with SqlEditorOverlay and use common API for overlays +export const SqlEditorOpenOverlay = observer(function SqlEditorOpenOverlay({ dataSource }) { + const translate = useTranslate(); + + function openHandler() { + dataSource?.open(); + } + + return ( + + + + {translate('plugin_sql_editor_action_overlay_title')} + + {translate('plugin_sql_editor_action_overlay_description')} + + + + + + ); +}); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.m.css b/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.m.css deleted file mode 100644 index 55f3d279e9..0000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.m.css +++ /dev/null @@ -1,3 +0,0 @@ -.overlayActions { - justify-content: space-between; -} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.module.css b/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.module.css new file mode 100644 index 0000000000..0627dc093a --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.module.css @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.overlayActions { + justify-content: space-between; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.tsx index 1f4ee566e0..c0530353fa 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import { OverlayHeaderTitle, OverlayMessage, s, + useFocus, useResource, useS, useTranslate, @@ -34,10 +35,10 @@ import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { NodeManagerUtils } from '@cloudbeaver/core-navigation-tree'; -import type { ISqlEditorTabState } from './ISqlEditorTabState'; -import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService'; -import style from './SqlEditorOverlay.m.css'; -import { SqlEditorService } from './SqlEditorService'; +import type { ISqlEditorTabState } from './ISqlEditorTabState.js'; +import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService.js'; +import style from './SqlEditorOverlay.module.css'; +import { SqlEditorService } from './SqlEditorService.js'; interface Props { state: ISqlEditorTabState; @@ -59,6 +60,7 @@ export const SqlEditorOverlay = observer(function SqlEditorOverlay({ stat executionContext ? createConnectionParam(executionContext.projectId, executionContext.connectionId) : null, ); const driver = useResource(SqlEditorOverlay, DBDriverResource, connection.tryGetData?.driverId ?? null); + const [focusedRef, { updateFocus }] = useFocus({ focusFirstChild: true }); const connected = getComputed(() => connection.tryGetData?.connected ?? false); @@ -90,25 +92,29 @@ export const SqlEditorOverlay = observer(function SqlEditorOverlay({ stat useEffect(() => { if (initExecutionContext && connected) { init(); + } else if (initExecutionContext && !connected) { + updateFocus(); } }, [connected, initExecutionContext]); return ( - - - - {connection.tryGetData?.name} - {dataContainer && {dataContainer}} - - {translate('sql_editor_restore_message')} - - - - - +
+ + + + {connection.tryGetData?.name} + {dataContainer && {dataContainer}} + + {translate('sql_editor_restore_message')} + + + + + +
); }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts index 46ae935668..87c82ea19c 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,45 +8,67 @@ import { observable } from 'mobx'; import { - Connection, + type Connection, ConnectionExecutionContextProjectKey, ConnectionExecutionContextResource, ConnectionExecutionContextService, ConnectionInfoResource, ConnectionsManagerService, createConnectionParam, - IConnectionExecutionContext, - IConnectionExecutionContextInfo, - IConnectionInfoParams, + type IConnectionExecutionContext, + type IConnectionExecutionContextInfo, + type IConnectionInfoParams, } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { FEATURE_GIT_ID, ServerConfigResource } from '@cloudbeaver/core-root'; -import { GraphQLService, SqlCompletionProposal, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk'; +import { GraphQLService, type SqlCompletionProposal, type SqlScriptInfoFragment } from '@cloudbeaver/core-sdk'; -import { getSqlEditorName } from './getSqlEditorName'; -import type { ISqlEditorTabState } from './ISqlEditorTabState'; -import { ESqlDataSourceFeatures } from './SqlDataSource/ESqlDataSourceFeatures'; -import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService'; -import { SqlEditorSettingsService } from './SqlEditorSettingsService'; +import { getSqlEditorName } from './getSqlEditorName.js'; +import type { ISqlEditorTabState } from './ISqlEditorTabState.js'; +import { ESqlDataSourceFeatures } from './SqlDataSource/ESqlDataSourceFeatures.js'; +import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService.js'; +import { SqlEditorSettingsService } from './SqlEditorSettingsService.js'; +import { Executor, type IExecutor } from '@cloudbeaver/core-executor'; +import { SqlQueryService } from './SqlResultTabs/SqlQueryService.js'; export type SQLProposal = SqlCompletionProposal; -export interface IQueryChangeData { - prevQuery: string; - query: string; - state: ISqlEditorTabState; +export interface ISqlEditorActiveQueryUpdateData { + editorId: string; + update: { + query: string; + type: 'replace' | 'append' | 'prepend'; + }; } -@injectable() +@injectable(() => [ + GraphQLService, + ConnectionsManagerService, + NotificationService, + ConnectionExecutionContextService, + ConnectionExecutionContextResource, + ConnectionInfoResource, + SqlDataSourceService, + SqlEditorSettingsService, + ServerConfigResource, + SqlQueryService, +]) export class SqlEditorService { - get autoSave() { - return this.sqlEditorSettingsService.settings.getValue('autoSave') && !this.serverConfigResource.isFeatureEnabled(FEATURE_GIT_ID, true); + get autoSave(): boolean { + return this.sqlEditorSettingsService.autoSave && !this.serverConfigResource.isFeatureEnabled(FEATURE_GIT_ID, true); } - readonly onQueryChange: ISyncExecutor; + get insertTableAlias() { + return this.sqlEditorSettingsService.insertTableAlias; + } + + /** + * This executor implemented in the useActiveQuery hook. + * It will work only when editor is mounted in the dom. + */ + readonly updateActiveQuery: IExecutor; constructor( private readonly graphQLService: GraphQLService, @@ -58,16 +80,20 @@ export class SqlEditorService { private readonly sqlDataSourceService: SqlDataSourceService, private readonly sqlEditorSettingsService: SqlEditorSettingsService, private readonly serverConfigResource: ServerConfigResource, + private readonly sqlQueryService: SqlQueryService, ) { - this.onQueryChange = new SyncExecutor(); + this.updateActiveQuery = new Executor(); + + this.sqlQueryService.onQueryExecution.addHandler(editorState => this.initEditorConnection(editorState)); } - getState(editorId: string, datasourceKey: string, order: number, source?: string): ISqlEditorTabState { + getState(editorId: string, datasourceKey: string, order: number, source?: string, metadata?: Record): ISqlEditorTabState { return observable({ editorId, datasourceKey, source, order, + metadata, tabs: observable([]), resultGroups: observable([]), resultTabs: observable([]), @@ -79,8 +105,9 @@ export class SqlEditorService { }); } - async parseSQLScript(connectionId: string, script: string): Promise { + async parseSQLScript(projectId: string, connectionId: string, script: string): Promise { const result = await this.graphQLService.sdk.parseSQLScript({ + projectId, connectionId, script, }); @@ -88,8 +115,9 @@ export class SqlEditorService { return result.scriptInfo; } - async parseSQLQuery(connectionId: string, script: string, position: number) { + async parseSQLQuery(projectId: string, connectionId: string, script: string, position: number) { const result = await this.graphQLService.sdk.parseSQLQuery({ + projectId, connectionId, script, position, @@ -99,6 +127,7 @@ export class SqlEditorService { } async getAutocomplete( + projectId: string, connectionId: string, contextId: string, query: string, @@ -107,6 +136,7 @@ export class SqlEditorService { simple?: boolean, ): Promise { const { proposals } = await this.graphQLService.sdk.querySqlCompletionProposals({ + projectId, connectionId, contextId, query, @@ -138,15 +168,10 @@ export class SqlEditorService { } } - setQuery(query: string, state: ISqlEditorTabState) { + setScript(script: string, state: ISqlEditorTabState) { const dataSource = this.sqlDataSourceService.get(state.editorId); - if (dataSource) { - const prevQuery = dataSource.script; - - dataSource.setScript(query); - this.onQueryChange.execute({ prevQuery, query, state }); - } + dataSource!.setScript(script); } async resetExecutionContext(state: ISqlEditorTabState) { @@ -182,7 +207,7 @@ export class SqlEditorService { } } - async initEditorConnection(state: ISqlEditorTabState): Promise { + initEditorConnection(state: ISqlEditorTabState): Promise { return this.sqlDataSourceService.executeAction( state.editorId, async dataSource => { diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts index 7d5628d927..9a7b20e492 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts @@ -1,104 +1,113 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 '@testing-library/jest-dom'; +import { describe } from 'vitest'; -import { coreAdministrationManifest } from '@cloudbeaver/core-administration'; -import { coreAppManifest } from '@cloudbeaver/core-app'; -import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; -import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; -import { coreBrowserManifest } from '@cloudbeaver/core-browser'; -import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; -import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; -import { coreEventsManifest } from '@cloudbeaver/core-events'; -import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; -import { coreProjectsManifest } from '@cloudbeaver/core-projects'; -import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; -import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; -import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; -import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; -import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; -import { coreRoutingManifest } from '@cloudbeaver/core-routing'; -import { coreSDKManifest } from '@cloudbeaver/core-sdk'; -import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { coreThemingManifest } from '@cloudbeaver/core-theming'; -import { coreUIManifest } from '@cloudbeaver/core-ui'; -import { coreViewManifest } from '@cloudbeaver/core-view'; -import { dataViewerManifest } from '@cloudbeaver/plugin-data-viewer'; -import { datasourceContextSwitchPluginManifest } from '@cloudbeaver/plugin-datasource-context-switch'; -import { navigationTabsPlugin } from '@cloudbeaver/plugin-navigation-tabs'; -import { navigationTreePlugin } from '@cloudbeaver/plugin-navigation-tree'; -import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; -import { createApp } from '@cloudbeaver/tests-runner'; +// import { coreAdministrationManifest } from '@cloudbeaver/core-administration'; +// import { coreAppManifest } from '@cloudbeaver/core-app'; +// import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; +// import { mockAuthentication } from '@cloudbeaver/core-authentication/__custom_mocks__/mockAuthentication.js'; +// import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +// import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; +// import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; +// import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; +// import { coreEventsManifest } from '@cloudbeaver/core-events'; +// import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; +// import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; +// import { coreProjectsManifest } from '@cloudbeaver/core-projects'; +// import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; +// import { createGQLEndpoint } from '@cloudbeaver/core-root/__custom_mocks__/createGQLEndpoint.js'; +// import '@cloudbeaver/core-root/__custom_mocks__/expectWebsocketClosedMessage.js'; +// import { mockAppInit } from '@cloudbeaver/core-root/__custom_mocks__/mockAppInit.js'; +// import { mockGraphQL } from '@cloudbeaver/core-root/__custom_mocks__/mockGraphQL.js'; +// import { mockServerConfig } from '@cloudbeaver/core-root/__custom_mocks__/resolvers/mockServerConfig.js'; +// import { coreRoutingManifest } from '@cloudbeaver/core-routing'; +// import { coreSDKManifest } from '@cloudbeaver/core-sdk'; +// import { coreSettingsManifest } from '@cloudbeaver/core-settings'; +// import { +// expectDeprecatedSettingMessage, +// expectNoDeprecatedSettingMessage, +// } from '@cloudbeaver/core-settings/__custom_mocks__/expectDeprecatedSettingMessage.js'; +// import { coreStorageManifest } from '@cloudbeaver/core-storage'; +// import { coreUIManifest } from '@cloudbeaver/core-ui'; +// import { coreViewManifest } from '@cloudbeaver/core-view'; +// import { dataViewerManifest } from '@cloudbeaver/plugin-data-viewer'; +// import { datasourceContextSwitchPluginManifest } from '@cloudbeaver/plugin-datasource-context-switch'; +// import { navigationTabsPlugin } from '@cloudbeaver/plugin-navigation-tabs'; +// import { navigationTreePlugin } from '@cloudbeaver/plugin-navigation-tree'; +// import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; +// import { createApp } from '@cloudbeaver/tests-runner'; -import { sqlEditorPluginManifest } from './manifest'; -import { SqlEditorSettings, SqlEditorSettingsService } from './SqlEditorSettingsService'; +// import { sqlEditorPluginManifest } from './manifest.js'; +// import { SqlEditorSettingsService } from './SqlEditorSettingsService.js'; -const endpoint = createGQLEndpoint(); -const app = createApp( - sqlEditorPluginManifest, - coreLocalizationManifest, - coreEventsManifest, - corePluginManifest, - coreProductManifest, - coreRootManifest, - coreSDKManifest, - coreBrowserManifest, - coreSettingsManifest, - coreViewManifest, - coreAuthenticationManifest, - coreProjectsManifest, - coreUIManifest, - coreRoutingManifest, - coreAdministrationManifest, - coreConnectionsManifest, - coreDialogsManifest, - coreNavigationTree, - coreAppManifest, - coreThemingManifest, - datasourceContextSwitchPluginManifest, - navigationTreePlugin, - navigationTabsPlugin, - objectViewerManifest, - dataViewerManifest, -); +// const endpoint = createGQLEndpoint(); +// const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); +// const app = createApp( +// sqlEditorPluginManifest, +// coreLocalizationManifest, +// coreEventsManifest, +// coreRootManifest, +// coreSDKManifest, +// coreBrowserManifest, +// coreSettingsManifest, +// coreViewManifest, +// coreStorageManifest, +// coreAuthenticationManifest, +// coreProjectsManifest, +// coreUIManifest, +// coreRoutingManifest, +// coreAdministrationManifest, +// coreConnectionsManifest, +// coreDialogsManifest, +// coreNavigationTree, +// coreAppManifest, +// datasourceContextSwitchPluginManifest, +// navigationTreePlugin, +// navigationTabsPlugin, +// objectViewerManifest, +// dataViewerManifest, +// coreClientActivityManifest, +// ); -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); +// const testValueNew = 1; +// const testValueDeprecated = 2; -beforeAll(() => app.init()); +// const deprecatedSettings = { +// 'core.app.sqlEditor.maxFileSize': testValueDeprecated, +// }; -const testValue = 1; +// const newSettings = { +// ...deprecatedSettings, +// 'plugin.sql-editor.maxFileSize': testValueNew, +// }; -const equalConfig = { - core: { - app: { - sqlEditor: { - maxFileSize: testValue, - } as SqlEditorSettings, - }, - }, - plugin: { - 'sql-editor': { - maxFileSize: testValue, - } as SqlEditorSettings, - }, -}; +// test('New settings override deprecated settings', async () => { +// const settings = app.serviceProvider.getService(SqlEditorSettingsService); +// const config = app.serviceProvider.getService(ServerConfigResource); -test('New settings equal deprecated settings', async () => { - const settings = app.injector.getServiceByClass(SqlEditorSettingsService); - const config = app.injector.getServiceByClass(ServerConfigResource); +// server.use(endpoint.query('serverConfig', mockServerConfig(newSettings))); - server.use(endpoint.query('serverConfig', mockServerConfig(equalConfig))); +// await config.refresh(); - await config.refresh(); +// expect(settings.maxFileSize).toBe(testValueNew); +// expectNoDeprecatedSettingMessage(); +// }); - expect(settings.settings.getValue('maxFileSize')).toBe(testValue); - expect(settings.deprecatedSettings.getValue('maxFileSize')).toBe(testValue); -}); +// test('Deprecated settings are used if new settings are not defined', async () => { +// const settings = app.serviceProvider.getService(SqlEditorSettingsService); +// const config = app.serviceProvider.getService(ServerConfigResource); + +// server.use(endpoint.query('serverConfig', mockServerConfig(deprecatedSettings))); + +// await config.refresh(); + +// expect(settings.maxFileSize).toBe(testValueDeprecated); +// expectDeprecatedSettingMessage(); +// }); + +describe.skip('SqlEditorSettingsService', () => {}); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts index f19b82a86c..77aa1b1dd1 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts @@ -1,29 +1,244 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2025 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 { injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; +import { + FEATURE_GIT_ID, + HIGHEST_SETTINGS_LAYER, + ServerConfigResource, + ServerSettingsManagerService, + SettingsTransformationService, +} from '@cloudbeaver/core-root'; +import { + createSettingsAliasResolver, + createSettingsOverrideResolver, + ESettingsValueType, + type ISettingDescription, + ROOT_SETTINGS_LAYER, + SettingsManagerService, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; -const defaultSettings = { - maxFileSize: 10 * 1024, // kilobyte - disabled: false, - autoSave: true, -}; +import { SQL_EDITOR_SETTINGS_GROUP } from './SQL_EDITOR_SETTINGS_GROUP.js'; -export type SqlEditorSettings = typeof defaultSettings; +const TABLE_ALIAS_OPTIONS = ['NONE', 'PLAIN', 'EXTENDED'] as const; +const ASSISTANT_MODE_OPTIONS = ['DEFAULT', 'NEW', 'COMBINE'] as const; -@injectable() +const TABLE_ALIAS_SETTING_OPTIONS = [ + { + value: 'NONE', + name: 'ui_disable', + }, + { + value: 'PLAIN', + name: 'my_table mt', + }, + { + value: 'EXTENDED', + name: 'my_table AS mt', + }, +]; + +const ASSISTANT_MODE_OPTIONS_LOCALIZED = [ + { value: 'DEFAULT', name: 'sql_editor_settings_content_assistant_experimental_mode_default' }, + { value: 'NEW', name: 'sql_editor_settings_content_assistant_experimental_mode_new' }, +]; + +const defaultSettings = schema.object({ + 'plugin.sql-editor.script.executionEnabled': schemaExtra.stringedBoolean().default(true), + 'plugin.sql-editor.maxFileSize': schema.coerce.number().default(10 * 1024), // kilobyte + 'plugin.sql-editor.disabled': schemaExtra.stringedBoolean().default(false), + 'plugin.sql-editor.autoSave': schemaExtra.stringedBoolean().default(true), + 'sql.proposals.insert.table.alias': schema.coerce + .string() + .transform(value => { + switch (value) { + case 'false': + return 'NONE'; + case 'true': + return 'PLAIN'; + default: + return value; + } + }) + .pipe(schema.enum(TABLE_ALIAS_OPTIONS)) + .default('PLAIN'), + 'SQLEditor.ContentAssistant.proposals.long.name': schema.coerce.boolean().default(false), + 'SQLEditor.ContentAssistant.experimental.mode': schema.coerce + .string() + .pipe(schema.enum(ASSISTANT_MODE_OPTIONS)) + .transform(value => { + switch (value) { + case 'DEFAULT': + return 'DEFAULT'; + default: + return 'NEW'; + } + }) + .default('NEW'), +}); + +type SqlEditorSettingsSchema = typeof defaultSettings; +export type SqlEditorSettings = schema.infer; + +@injectable(() => [ + SettingsProviderService, + SettingsManagerService, + SettingsResolverService, + SettingsTransformationService, + ServerSettingsManagerService, + ServerConfigResource, +]) export class SqlEditorSettingsService { - readonly settings: PluginSettings; - /** @deprecated Use settings instead, will be removed in 23.0.0 */ - readonly deprecatedSettings: PluginSettings; + get scriptExecutionEnabled(): boolean { + return this.settings.getValue('plugin.sql-editor.script.executionEnabled'); + } + get maxFileSize(): number { + return this.settings.getValue('plugin.sql-editor.maxFileSize'); + } + + get disabled(): boolean { + return this.settings.getValue('plugin.sql-editor.disabled'); + } + + get autoSave(): boolean { + return this.settings.getValue('plugin.sql-editor.autoSave'); + } + + get insertTableAlias(): schema.infer['sql.proposals.insert.table.alias'] { + return this.settings.getValue('sql.proposals.insert.table.alias'); + } + + get longNameProposals(): boolean { + return this.settings.getValue('SQLEditor.ContentAssistant.proposals.long.name'); + } + + readonly settings: SettingsProvider; + + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + private readonly settingsResolverService: SettingsResolverService, + private readonly settingsTransformationService: SettingsTransformationService, + private readonly serverSettingsManagerService: ServerSettingsManagerService, + private readonly serverConfigResource: ServerConfigResource, + ) { + this.settings = this.settingsProviderService.createSettings(defaultSettings); + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, + /** @deprecated Use settings instead, will be removed in 23.0.0 */ + createSettingsAliasResolver(this.settingsProviderService.settingsResolver, { + 'plugin.sql-editor.autoSave': 'core.app.sqlEditor.autoSave', + 'plugin.sql-editor.maxFileSize': 'core.app.sqlEditor.maxFileSize', + 'plugin.sql-editor.disabled': 'core.app.sqlEditor.disabled', + }), + ); + this.settingsResolverService.addResolver( + HIGHEST_SETTINGS_LAYER, + createSettingsOverrideResolver(this.settingsProviderService.settingsResolver, { + 'plugin.sql-editor.script.executionEnabled': { + key: 'permission.sql.script.execution', + filter: value => !value, + }, + }), + ); + this.registerSettings(); + } + + private registerSettings() { + this.settingsTransformationService.setGroupOverride('editors/sqlEditor', SQL_EDITOR_SETTINGS_GROUP); + this.settingsTransformationService.setSettingTransformer( + 'sql.proposals.insert.table.alias', + setting => + ({ + ...setting, + group: SQL_EDITOR_SETTINGS_GROUP, + name: 'sql_editor_settings_insert_table_aliases_name', + description: 'sql_editor_settings_insert_table_aliases_desc', + options: [...(setting.options?.filter(option => !TABLE_ALIAS_OPTIONS.includes(option.value as any)) || []), ...TABLE_ALIAS_SETTING_OPTIONS], + }) as ISettingDescription, + ); + this.settingsTransformationService.setSettingTransformer( + 'SQLEditor.ContentAssistant.experimental.mode', + setting => + ({ + ...setting, + group: SQL_EDITOR_SETTINGS_GROUP, + name: 'sql_editor_settings_content_assistant_experimental_mode_name', + description: 'sql_editor_settings_content_assistant_experimental_mode_desc', + options: ASSISTANT_MODE_OPTIONS_LOCALIZED, + }) as ISettingDescription, + ); + + this.settingsManagerService.registerSettings(() => { + const settings: ISettingDescription[] = [ + { + group: SQL_EDITOR_SETTINGS_GROUP, + key: 'plugin.sql-editor.disabled', + access: { + scope: ['role'], + }, + type: ESettingsValueType.Checkbox, + name: 'plugin_sql_editor_settings_disable', + description: 'plugin_sql_editor_settings_disable_description', + }, + { + group: SQL_EDITOR_SETTINGS_GROUP, + key: 'plugin.sql-editor.maxFileSize', + access: { + scope: ['client', 'server'], + }, + type: ESettingsValueType.Input, + name: 'plugin_sql_editor_settings_import_max_size', + description: 'plugin_sql_editor_settings_import_max_size_description', + }, + { + group: SQL_EDITOR_SETTINGS_GROUP, + key: 'plugin.sql-editor.autoSave', + access: { + scope: ['client', 'server'], + }, + type: ESettingsValueType.Checkbox, + name: 'plugin_sql_editor_settings_auto_save', + description: this.serverConfigResource.isFeatureEnabled(FEATURE_GIT_ID, true) + ? 'plugin_sql_editor_settings_auto_save_description_git_integration' + : 'plugin_sql_editor_settings_auto_save_description', + }, + ]; - constructor(private readonly pluginManagerService: PluginManagerService) { - this.settings = this.pluginManagerService.createSettings('sql-editor', 'plugin', defaultSettings); - this.deprecatedSettings = this.pluginManagerService.getDeprecatedPluginSettings('core.app.sqlEditor', defaultSettings); + if (!this.serverSettingsManagerService.providedSettings.has('sql.proposals.insert.table.alias')) { + settings.push({ + key: 'sql.proposals.insert.table.alias', + access: { + scope: ['server', 'client'], + }, + group: SQL_EDITOR_SETTINGS_GROUP, + type: ESettingsValueType.Select, + name: 'sql_editor_settings_insert_table_aliases_name', + description: 'sql_editor_settings_insert_table_aliases_desc', + options: TABLE_ALIAS_SETTING_OPTIONS, + }); + } + if (!this.serverSettingsManagerService.providedSettings.has('SQLEditor.ContentAssistant.experimental.mode')) { + settings.push({ + key: 'SQLEditor.ContentAssistant.experimental.mode', + access: { + scope: ['server', 'client'], + }, + group: SQL_EDITOR_SETTINGS_GROUP, + type: ESettingsValueType.Select, + name: 'sql_editor_settings_content_assistant_experimental_mode_name', + options: ASSISTANT_MODE_OPTIONS_LOCALIZED, + }); + } + return settings; + }); } } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorStatusBar.module.css b/webapp/packages/plugin-sql-editor/src/SqlEditorStatusBar.module.css new file mode 100644 index 0000000000..891b3f2572 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorStatusBar.module.css @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.loader { + composes: theme-background-surface theme-border-color-background from global; + position: absolute; + bottom: 0px; + border-top: 1px solid; + width: 100%; + padding: 0 8px; + box-sizing: border-box; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorStatusBar.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditorStatusBar.tsx index a9b411dcf7..59dc27d386 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorStatusBar.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorStatusBar.tsx @@ -1,35 +1,22 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; -import { Loader } from '@cloudbeaver/core-blocks'; +import { Loader, s, useS } from '@cloudbeaver/core-blocks'; -import type { ISqlDataSource } from './SqlDataSource/ISqlDataSource'; - -const viewerStyles = css` - Loader { - composes: theme-background-surface theme-border-color-background from global; - - position: absolute; - bottom: 0px; - - border-top: 1px solid; - width: 100%; - padding: 0 8px; - box-sizing: border-box; - } -`; +import type { ISqlDataSource } from './SqlDataSource/ISqlDataSource.js'; +import classes from './SqlEditorStatusBar.module.css'; interface Props { dataSource: ISqlDataSource | undefined; } export const SqlEditorStatusBar = observer(function SqlEditorStatusBar({ dataSource }) { - return styled(viewerStyles)(); + const styles = useS(classes); + return ; }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts index 0178d42e5c..64a519a537 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts @@ -1,22 +1,22 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; -import { ACTION_REDO, ACTION_SAVE, ACTION_UNDO, IActiveView, View } from '@cloudbeaver/core-view'; -import { ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; +import { ACTION_REDO, ACTION_SAVE, ACTION_UNDO, type IActiveView, View } from '@cloudbeaver/core-view'; +import { type ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; -import { ACTION_SQL_EDITOR_EXECUTE } from './actions/ACTION_SQL_EDITOR_EXECUTE'; -import { ACTION_SQL_EDITOR_EXECUTE_NEW } from './actions/ACTION_SQL_EDITOR_EXECUTE_NEW'; -import { ACTION_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/ACTION_SQL_EDITOR_EXECUTE_SCRIPT'; -import { ACTION_SQL_EDITOR_FORMAT } from './actions/ACTION_SQL_EDITOR_FORMAT'; -import { ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN'; -import { ACTION_SQL_EDITOR_SHOW_OUTPUT } from './actions/ACTION_SQL_EDITOR_SHOW_OUTPUT'; +import { ACTION_SQL_EDITOR_EXECUTE } from './actions/ACTION_SQL_EDITOR_EXECUTE.js'; +import { ACTION_SQL_EDITOR_EXECUTE_NEW } from './actions/ACTION_SQL_EDITOR_EXECUTE_NEW.js'; +import { ACTION_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/ACTION_SQL_EDITOR_EXECUTE_SCRIPT.js'; +import { ACTION_SQL_EDITOR_FORMAT } from './actions/ACTION_SQL_EDITOR_FORMAT.js'; +import { ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN.js'; +import { ACTION_SQL_EDITOR_SHOW_OUTPUT } from './actions/ACTION_SQL_EDITOR_SHOW_OUTPUT.js'; -@injectable() +@injectable(() => [NavigationTabsService]) export class SqlEditorView extends View { constructor(private readonly navigationTabsService: NavigationTabsService) { super(); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/DATA_CONTEXT_SQL_EDITOR_RESULT_ID.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/DATA_CONTEXT_SQL_EDITOR_RESULT_ID.ts index 3195bd3d26..4cdc0e4594 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/DATA_CONTEXT_SQL_EDITOR_RESULT_ID.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/DATA_CONTEXT_SQL_EDITOR_RESULT_ID.ts @@ -1,12 +1,12 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { createDataContext } from '@cloudbeaver/core-data-context'; -import type { ISqlEditorResultTab } from '../ISqlEditorTabState'; +import type { ISqlEditorResultTab } from '../ISqlEditorTabState.js'; export const DATA_CONTEXT_SQL_EDITOR_RESULT_ID = createDataContext('sql-editor-result-id'); diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.m.css b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.m.css deleted file mode 100644 index 7e486d15ce..0000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.m.css +++ /dev/null @@ -1,14 +0,0 @@ -.pane { - composes: theme-background-surface theme-text-on-surface from global; -} -.split { - height: 100%; - flex-direction: column; -} -.resizerControls { - width: 100%; - height: 2px; -} -.textarea > :global(textarea) { - border: none !important; -} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.module.css b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.module.css new file mode 100644 index 0000000000..b3b8637a52 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.module.css @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.pane { + composes: theme-background-surface theme-text-on-surface from global; +} +.split { + height: 100%; + flex-direction: column; +} +.resizerControls { + width: 100%; + height: 2px; +} +.textarea > :global(textarea) { + border: none !important; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx index 22fdd2a4a0..abd6111b21 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/ExecutionPlan/ExecutionPlanTreeBlock.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 DBeaver Corp and others + * Copyright (C) 2020-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -24,9 +24,9 @@ import { } from '@cloudbeaver/core-blocks'; import type { SqlExecutionPlanNode } from '@cloudbeaver/core-sdk'; -import style from './ExecutionPlanTreeBlock.m.css'; -import { NestedNode } from './NestedNode'; -import { useExecutionPlanTreeState } from './useExecutionPlanTreeState'; +import style from './ExecutionPlanTreeBlock.module.css'; +import { NestedNode } from './NestedNode.js'; +import { useExecutionPlanTreeState } from './useExecutionPlanTreeState.js'; interface Props { nodeList: SqlExecutionPlanNode[]; @@ -43,9 +43,9 @@ export const ExecutionPlanTreeBlock = observer(function ExecutionPlanTree return ( - + {state.nodes.length && state.columns.length ? ( - +
{state.columns.map(property => { const name = property.displayName; @@ -67,8 +67,8 @@ export const ExecutionPlanTreeBlock = observer(function ExecutionPlanTree {translate('sql_execution_plan_placeholder')} )} - - + +